From fb1b2290a525a75f7a75a16c7d1033e041cab1e1 Mon Sep 17 00:00:00 2001 From: nbkdoesntknowcoding Date: Wed, 26 Aug 2026 21:05:39 +0530 Subject: [PATCH] docs: the furniture an open source repo is expected to have MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit None of this existed: no security policy, no issue templates, no pull request template, and nothing telling an agent how to work here. SECURITY.md draws the line that matters for this project specifically. The interesting surface is who may reach the video and input channels — pairing bypass, token handling, injection without pairing, the helper's local IPC. It also says plainly what is NOT a vulnerability, because all three get reported: the binaries are unsigned by choice, the private API is the central technique rather than an oversight, and the recording indicator appearing is correct behaviour. The issue templates are shaped by what actually goes wrong. "It will not connect" is its own template rather than a bug report, because it is the common failure and it needs different questions — and it opens by ruling out the two causes that look like faults and are not: a Mac that is running but not sharing, and a second receiver already holding the connection. Today, both of those cost a session each. The template asks for the receiver's exact wording, since a named refusal and "Reconnecting…" are different problems. AGENTS.md is the emerging convention for this, and there was a natural thing to put in it: the setup prompt that was buried in the README, which now lives in one place. Alongside it, the four conventions that mislead anyone assuming the usual ones — the Xcode project is generated, the protocol has golden vectors, macOS permissions cannot be granted by any script, and mac/build/ must never be committed. It also records how this codebase tests things that need two machines, and the failure it keeps repeating: the app knows a specific fact and displays a generic one. That has now shipped four times in different clothes, and it is worth a paragraph where the next person writing user-facing state will read it. The pull request template asks what was NOT verified, and asks for anything a reviewer should be suspicious of. Both because a change that builds and passes but was never run is a normal thing to submit, and saying so is what makes it safe to review. Repository topics were set separately, via the API: sixteen of them, where there had been none. Co-Authored-By: Claude Opus 5 --- .github/ISSUE_TEMPLATE/bug.yml | 51 +++++++++ .github/ISSUE_TEMPLATE/config.yml | 8 ++ .github/ISSUE_TEMPLATE/connection.yml | 76 +++++++++++++ .github/pull_request_template.md | 21 ++++ AGENTS.md | 152 ++++++++++++++++++++++++++ README.md | 68 ++---------- SECURITY.md | 56 ++++++++++ 7 files changed, 372 insertions(+), 60 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/connection.yml create mode 100644 .github/pull_request_template.md create mode 100644 AGENTS.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..8248234 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,51 @@ +name: Bug report +description: Something behaves differently from how it is documented +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Please check **Known limits** in the README first. The 60 Hz ceiling, the + ~1920×1200 geometry ceiling, and protected video refusing to play are + properties of the private display API rather than defects, and are not + going to change. + + If the receiver cannot connect at all, the *It will not connect* + template asks better questions than this one. + - type: input + id: versions + attributes: + label: Versions + placeholder: "Mac 0.16.1, receiver 0.16.1, macOS 26.2, Windows 11" + validations: + required: true + - type: textarea + id: expected + attributes: + label: What did you expect to happen? + validations: + required: true + - type: textarea + id: actual + attributes: + label: What happened instead? + validations: + required: true + - type: textarea + id: steps + attributes: + label: How can it be reproduced? + description: > + Including what was on the second display at the time. An idle desktop + legitimately sends almost no frames, so "0 fps" on an empty screen is + correct rather than a fault. + validations: + required: true + - type: textarea + id: hud + attributes: + label: The receiver's HUD, if the stream was running + description: > + Press H to show it. The per-stage rows say far more than a description + of the symptom can. + render: text diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..b85310e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Why the app is unsigned, and what the warnings mean + url: https://github.com/nbkdoesntknowcoding/display-share/blob/main/docs/distribution.md + about: The macOS and SmartScreen warnings on first launch are expected and explained here. + - name: Security vulnerability + url: https://github.com/nbkdoesntknowcoding/display-share/security/advisories/new + about: Please report these privately rather than as a public issue. diff --git a/.github/ISSUE_TEMPLATE/connection.yml b/.github/ISSUE_TEMPLATE/connection.yml new file mode 100644 index 0000000..66fb438 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/connection.yml @@ -0,0 +1,76 @@ +name: It will not connect +description: The receiver cannot find the Mac, or connects and immediately drops +labels: ["connection"] +body: + - type: markdown + attributes: + value: | + Most connection reports come down to one of a handful of causes, and the + questions below are the ones that separate them. Answering them saves a + round trip each. + + **Two worth ruling out before filing**, because they look like faults and + are not: + + - **Is the Mac actually sharing?** Running is not the same as sharing. + The menu bar panel has to say it is sharing — if it says *Press Start + to begin*, the receiver has nothing to connect to. + - **Is another receiver already attached?** Only one at a time is + allowed. A second one is refused, and the receiver will keep retrying. + Close any other receiver, including a browser tab open on the Mac's + own viewer page. + - type: input + id: versions + attributes: + label: Versions + description: Both halves. The receiver shows its version on the connect card. + placeholder: "Mac 0.16.1, receiver 0.16.1" + validations: + required: true + - type: dropdown + id: stage + attributes: + label: How far does it get? + options: + - The receiver never lists the Mac at all + - It lists the Mac, but connecting fails + - It connects, then drops straight away + - It connects and then drops after a while + - It asks for a PIN and the PIN is refused + validations: + required: true + - type: textarea + id: message + attributes: + label: What does the receiver say, word for word? + description: > + The exact text matters. "Disconnected. Reconnecting…" and a named + refusal are different problems with different causes. + validations: + required: true + - type: textarea + id: mac-state + attributes: + label: What does the Mac's menu bar panel say? + placeholder: "Sharing, or Not sharing — and anything under it" + validations: + required: true + - type: dropdown + id: network + attributes: + label: How are the two machines connected? + options: + - Same Wi-Fi network + - Same router, one or both on Ethernet + - A cable straight between the two machines + - Different networks, or not sure + validations: + required: true + - type: textarea + id: extra + attributes: + label: Anything else + description: > + Guest or workplace Wi-Fi often blocks the discovery protocol, which + shows up as the Mac never being listed. Typing the Mac's address by + hand tells the two cases apart — please say whether that works. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..3bfa566 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +## What changes, and why + + + +## How it was verified + + + +- [ ] `xcodebuild -scheme DisplayShareCore -derivedDataPath ./.build test` (from `mac/`) +- [ ] `cargo test --manifest-path src-tauri/Cargo.toml` (from `windows/`) +- [ ] The `node --experimental-strip-types scripts/verify-*.mjs` checks (from `windows/`) +- [ ] Run against a real Mac and receiver + +## Anything a reviewer should be suspicious of + + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e5f25b0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,152 @@ +# Working in this repository + +Notes for coding agents. [CONTRIBUTING.md](CONTRIBUTING.md) covers the licensing +rules; this covers how to build, what to run, and the specific ways this codebase +will mislead you if you assume the usual conventions. + +## The shape of it + +Two applications that talk over a documented wire protocol. + +| Path | What it is | +|---|---| +| `mac/DisplayShare/` | SwiftUI menu bar app — no window, `LSUIElement` | +| `mac/DisplayShareCore/` | capture, encode, transport, pairing, input. Most logic lives here | +| `mac/vd_helper/` | separate process that owns the `CGVirtualDisplay` | +| `mac/Shared/` | wire protocol and helper IPC, compiled into both | +| `windows/src/` | TypeScript frontend: WebCodecs decode, canvas, input | +| `windows/src-tauri/` | Rust backend that owns the WebSocket | +| `protocol/` | `SPEC.md` and golden vectors | + +## Four things that will catch you out + +**The Xcode project is generated.** Edit `mac/project.yml`, then run +`xcodegen generate`. Changes made directly to `DisplayShare.xcodeproj` are +overwritten, including new files — a test file added only to the `.xcodeproj` +silently never runs. + +**The protocol has golden vectors.** The Swift and TypeScript parsers are each +tested against the same bytes in `protocol/vectors/`, never against each other, +so a shared misunderstanding cannot pass. Changing the wire format means +revising `protocol/SPEC.md` and both implementations. Do not add a field to the +header for convenience. + +**macOS permissions cannot be granted by a script.** Screen Recording and +Accessibility are granted by a human in System Settings, and a freshly built +copy is a *new identity* even if permission was granted to a previous build. If +a task needs them, stop and ask. There is no API, entitlement or `sudo` that +does it. + +**`mac/build/` is hundreds of megabytes** and must never be committed. It is +ignored; do not add it back with `git add -f`. + +## Commands + +```bash +# Mac +brew install xcodegen +cd mac && xcodegen generate +xcodebuild -scheme DisplayShareCore -derivedDataPath ./.build test + +# Windows receiver — the Rust half builds and tests on macOS too +cd windows && npm ci +cargo test --manifest-path src-tauri/Cargo.toml +npm run build + +# Frontend checks, all run in CI +node scripts/verify-vectors.mjs +node --experimental-strip-types scripts/verify-window-states.mjs +node --experimental-strip-types scripts/verify-timing.mjs +node --experimental-strip-types scripts/verify-refusals.mjs +``` + +## How this codebase tests things that need two machines + +Almost every defect that ever reached a user here passed a green build, because +the failing path needed a Mac, a receiver and a network to reach. The response +has been consistent, and new work should follow it: + +**Extract the decision as a pure value, then assert it.** `SendGate` decides +what to shed without a socket. `Cadence` picks a frame rate without a display. +`PopoverLayout.ordered` returns the sections without a view. `DisplayPlacement` +answers which edge without a second monitor. Frontend equivalents live in +`windows/src/*.ts` with a `scripts/verify-*.mjs` runner wired into both CI jobs. + +**Then break it on purpose.** An assertion that still passes with the fix +reverted is worth nothing, and this project has written a few. Delete the guard, +watch a named test fail, put it back. If nothing fails, the test is decoration — +say so rather than keeping it. + +## The failure this project keeps repeating + +Worth knowing before writing anything user-facing, because it has shipped +repeatedly in different clothes: **the app knows a specific fact and displays a +generic one.** + +A refused session showed "Reconnecting…". A display placed to the left said +nothing at all. An idle desktop was read as a dead capture and as a congested +link, and both times something was restarted or degraded for it. In every case +the information existed and the interface declined to say it, and in every case +the user reasonably concluded the app was broken — because from outside there is +no difference. + +When adding a state, the question is not "what should this show" but "what does +the app already know here that it is not saying". + +## Setting it up on a machine + +
+A prompt that knows where the human steps are + +```text +Set up Display Share on this machine. It turns a Windows laptop into a real +second display for a Mac. Repo: https://github.com/nbkdoesntknowcoding/display-share + +MAC SENDER (only runs on macOS 14+): +1. git clone https://github.com/nbkdoesntknowcoding/display-share.git && cd display-share +2. Run ./install.sh and show me its output. It installs xcodegen if needed, + builds a Release build, and installs to /Applications. +3. If it fails, read the build log path it prints and fix or report the error. + Do NOT skip failures or fall back to a Debug build. + +THEN STOP AND ASK ME. You cannot do the next part: +macOS permissions are granted by a human in System Settings — no command, +script, or API can grant them, and a freshly built copy counts as a NEW app +identity even if I granted them before. Tell me to: + - System Settings > Privacy & Security > Screen Recording > enable Display Share + - (only if I want to control the Mac from the laptop) + Privacy & Security > Accessibility > enable Display Share +Then wait for me to confirm. + +VERIFY (after I confirm): +4. Launch /Applications/DisplayShare.app. It is a MENU BAR app — no Dock icon, + no window. Click its icon and press Start. +5. Confirm a virtual display exists: `system_profiler SPDisplaysDataType | grep -i display` + should show one more display than the physical monitors. +6. Open http://localhost:8787 in a browser on the Mac. Expect a black canvas and + a HUD. It stays black until something is actually on that display — drag a + window onto the new display, then the HUD should show frames arriving. + A near-black screen with capture near 0 fps is CORRECT for an empty desktop. + NOTE: this browser page counts as a receiver, and only one is allowed at a + time. Close it before connecting the real receiver, or that one is refused. + +WINDOWS RECEIVER (run this part on the Windows laptop): +7. Install Rust (https://rustup.rs) and Node 22+. +8. cd windows && npm ci && npx tauri build +9. Run the installer from windows/src-tauri/target/release/bundle/nsis/ + SmartScreen will warn because it is unsigned: More info > Run anyway. +10. The app finds the Mac over Bonjour. Enter the 4-digit PIN the Mac shows. + Both machines must be on the same network, on 5 GHz Wi-Fi or Ethernet. + +USEFUL TO KNOW: +- Ports 8787 and 8788 must not be blocked. +- Keys in the receiver: F fullscreen, H toggle HUD, K force a keyframe, + A cycle decode mode, F8 forward input to the Mac. +- macOS decides where the second display goes and usually puts it on the LEFT. + Push the cursor that way to reach it; System Settings > Displays moves it. +- Read README.md "Known limits" before reporting a bug — the 60 Hz cap, the + ~1920x1200 geometry ceiling and no-HDCP are properties of Apple's private + API, not defects. +``` + +
diff --git a/README.md b/README.md index 11ebeda..e25cf9d 100644 --- a/README.md +++ b/README.md @@ -159,64 +159,11 @@ xcodebuild -scheme DisplayShare -configuration Release -derivedDataPath ./.build cd windows && npm ci && npx tauri build ``` -
-Or hand it to a coding agent — a prompt that knows where the human steps are - -Paste this into Claude Code, Cursor, or any agent with shell access. The -important part is that it stops before the permission grant, because no script -can grant macOS permissions: - -````text -Set up Display Share on this machine. It turns a Windows laptop into a real -second display for a Mac. Repo: https://github.com/nbkdoesntknowcoding/display-share - -MAC SENDER (only runs on macOS 14+): -1. git clone https://github.com/nbkdoesntknowcoding/display-share.git && cd display-share -2. Run ./install.sh and show me its output. It installs xcodegen if needed, - builds a Release build, and installs to /Applications. -3. If it fails, read the build log path it prints and fix or report the error. - Do NOT skip failures or fall back to a Debug build. - -THEN STOP AND ASK ME. You cannot do the next part: -macOS permissions are granted by a human in System Settings — no command, -script, or API can grant them, and a freshly built copy counts as a NEW app -identity even if I granted them before. Tell me to: - - System Settings > Privacy & Security > Screen Recording > enable Display Share - - (only if I want to control the Mac from the laptop) - Privacy & Security > Accessibility > enable Display Share -Then wait for me to confirm. - -VERIFY (after I confirm): -4. Launch /Applications/DisplayShare.app. It is a MENU BAR app — no Dock icon, - no window. Click its icon and press Start. -5. Confirm a virtual display exists: `system_profiler SPDisplaysDataType | grep -i display` - should show one more display than the physical monitors. -6. Open http://localhost:8787 in a browser on the Mac. Expect a black canvas and - a HUD. It stays black until something is actually on that display — drag a - window onto the new display, then the HUD should show ~58 fps. - A near-black screen with capture near 0 fps is CORRECT for an empty desktop - — that is not a bug. - -WINDOWS RECEIVER (run this part on the Windows laptop): -7. Install Rust (https://rustup.rs) and Node 22+. -8. cd windows && npm ci && npx tauri build -9. Run the installer from windows/src-tauri/target/release/bundle/nsis/ - SmartScreen will warn because it is unsigned: More info > Run anyway. -10. The app finds the Mac over Bonjour. Enter the 4-digit PIN the Mac shows. - Both machines must be on the same network, on 5 GHz Wi-Fi or Ethernet. - -USEFUL TO KNOW: -- Ports 8787 (viewer page) and 8788 (video + control) must not be blocked. -- Keys in the receiver: F fullscreen, H toggle HUD, K force a keyframe, - A cycle decode mode, F8 forward input to the Mac. -- Read README.md "Known limits" before reporting a bug — the 60 Hz cap, the - ~1920x1200 geometry ceiling and no-HDCP are properties of Apple's private - API, not defects. -- If anything is ambiguous, read docs/distribution.md and protocol/SPEC.md - rather than guessing. -```` - -
+### Or hand it to a coding agent + +[AGENTS.md](AGENTS.md) has a setup prompt written for agents with shell access. +It stops before the permission grant, because no script can grant macOS +permissions — that part is always a human in System Settings. --- @@ -538,8 +485,9 @@ see [What isn't proven yet](#what-isnt-proven-yet). A HUD screenshot from a real Mac-to-Windows session over a real LAN would settle several open questions at once. -Found a security issue? Please open a private advisory through GitHub's -**Security** tab rather than a public issue. +Found a security issue? [SECURITY.md](SECURITY.md) says how to report it +privately, and what counts — the unsigned binaries and the private API use are +deliberate, documented trade-offs rather than vulnerabilities. --- diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..1091023 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,56 @@ +# Security + +## Reporting + +Please report vulnerabilities **privately**, through this repository's +**Security** tab → *Report a vulnerability*. That opens a draft advisory only +maintainers can see. + +Please do not open a public issue for anything in the "in scope" list below. + +This is a small project with no security team and no paid support. A realistic +expectation is a first reply within a week. If something is being actively +exploited, say so in the title and it will be looked at sooner. + +## In scope + +Display Share puts a video stream and an input channel on your local network, so +the interesting failures are about who is allowed to reach them: + +- **Pairing bypass** — receiving video, or having input accepted, without + completing the PIN exchange. Input forwarding in particular is refused until a + device is paired, and a way around that is the highest-severity report here. +- **Token handling** — pairing tokens are stored hashed. A way to recover a + usable token from what is written to disk, or to replay one from another + device, is in scope. +- **Input injection** — the Mac injects `CGEvent`s on behalf of a paired + receiver. Anything that lets an unpaired party drive that is in scope. +- **The helper process** — `vd_helper` holds the virtual display and speaks a + local IPC protocol. Anything that lets an unrelated local process drive it, or + escalate through it, is in scope. +- **Path handling in the updater** — both apps download release assets. Anything + that writes outside the intended location is in scope. + +## Not vulnerabilities + +Stated plainly, because these get reported and they are deliberate: + +- **The binaries are unsigned.** Display Share is open source and does not buy + code signing certificates, so macOS and SmartScreen both warn on first launch. + That is a known, documented trade — see [docs/distribution.md](docs/distribution.md). + Build it yourself and the warning does not appear. +- **It uses a private Apple API.** `CGVirtualDisplay` is how a real virtual + display is created at all. It is the project's central technique, not an + oversight, and it is why this cannot ship on the Mac App Store. +- **It records the screen.** It captures the virtual display it creates, and + macOS shows the purple recording indicator exactly as it does for any other + capture app. That indicator appearing is correct behaviour. +- **Traffic on your LAN is not encrypted.** The stream is plain WebSocket on the + local network, gated by pairing. If your threat model includes someone on your + own LAN reading it, this is not the tool for you today — but a report saying + so is a feature request, and a welcome one, rather than a vulnerability. + +## Scope of the code + +Only this repository. The reference projects named in the README are separate +and should be reported to their own maintainers.