ReviewOS

stacks/uplink

docs(homepage): the page never discloses bypassPermissions or where texts go

#27
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Part of #6.

The page asks a visitor to grant Full Disk Access and let an agent run unattended on their Mac, and it does not tell them the two things that decide whether that is acceptable: approvals are off by default, and the text plus whatever files the agent opens go to Anthropic through the CLI. Both facts are already in the code. A reader who installs on the strength of the one security line and finds out afterwards has been surprised by the part of the product that most needed saying up front.

What is there today

  • The whole security story is two lines in one bento cell, resources/views/index.stx#L308-L309: "Nobody else can use it" plus "Only one-to-one chats with handles you allow. Group chats are ignored, and only Uplink.app gets Full Disk Access, not your terminal."
  • One FAQ restates the same thing at resources/views/index.stx#L73-L74, and its answer ("Anything Claude Code can: search the web, read and edit files, run your tests") describes the capability without saying it is exercised with no prompts.
  • app/Uplink/config.ts#L67 defaults permissionMode to bypassPermissions. The value is passed straight to the CLI as --permission-mode at app/Uplink/engine.ts#L71-L72, from both entry points: the from-source service at app/Commands/Uplink.ts#L25 and the downloadable app at app/Uplink/desktop-agent.ts#L178. UPLINK_PERMISSION_MODE is the only override, and it appears nowhere in the UPLINK_* block of .env.example#L141-L160, so a from-source user cannot discover it from the file they are told to copy.
  • The page's only data-flow statement is resources/views/index.stx#L245: "No server in between. Your Mac reads the message, does the work and answers through Messages." That is true of Uplink, and a reader will take it to mean nothing leaves the Mac.
  • The allowlist facts worth stating are all in app/Uplink/filter.ts, which names itself the security boundary of the app at app/Uplink/filter.ts#L7-L9. Every branch of classify rejects rather than admits: group chat at app/Uplink/filter.ts#L37-L38, a chat not on the allowlist at #L40-L41, an outgoing message in someone else's chat at #L43-L48, a sender that does not match the chat at #L49-L55, tapbacks, edit markers and system items at #L57-L58, empty text at #L60-L62, and Uplink's own reply coming back around at #L64-L68.
  • An empty allowlist means this Mac's own Messages handles, not everyone: app/Uplink/uplink.ts#L202. When neither a configured list nor an own handle is available, every text is ignored and the log says so, app/Uplink/uplink.ts#L134.
  • catchUpMs is 30 minutes at app/Uplink/config.ts#L57, so a Mac waking up does not replay a day of texts. The reply prefix at app/Uplink/config.ts#L58 is half of the loop guard.
  • Local state is genuinely local. The downloadable app keeps one SQLite file in Application Support, app/Uplink/sqlite-store.ts#L7-L8 and app/Uplink/settings.ts#L16. The token lives in the Keychain and not in that file, app/Uplink/settings.ts#L9-L11, #L19-L20 and #L74-L78. The dashboard refuses to render outside a local environment, resources/views/dashboard.stx#L12-L17. The menubar popover is served on loopback with cross-site requests refused, app/Uplink/desktop-agent.ts#L277-L286. No file under app/Uplink/ makes an outbound HTTP request.
  • Full Disk Access goes to the app rather than to bun, because TCC charges a child process's access to the app that launched it, app/Uplink/launcher.ts#L5-L10. The exception is running the watcher in a terminal, which does need the grant for that terminal, README.md#L104.
  • The page itself is the one thing that does phone home. resources/views/index.stx#L104-L106 preconnects to fonts.googleapis.com and fonts.gstatic.com and loads Geist and Geist Mono from Google, and resources/views/404.stx#L16-L18 loads Geist the same way. There is no public/fonts/ directory, so every visitor's IP reaches Google before the first paint.

Proposal

  1. Add one section to resources/views/index.stx between the bento section's closing tag at index.stx#L314 and the download section at index.stx#L316, with an id and scroll-mt-16 matching the other anchored sections at index.stx#L240 and index.stx#L316, and a nav link beside the two at index.stx#L181-L182. No new route and no new view.
  2. State the permission model plainly in that section: the agent runs with bypassPermissions, which means no approval prompts, so a text that is accepted gets unattended read, write and execute on the Mac. Name UPLINK_PERMISSION_MODE as the way to change it. Say that an unlocked or stolen iPhone with access to the allowed thread is therefore a compromise of the Mac.
  3. State the limits that make that acceptable, each of which is true today: the allowlist is fail closed and rejects by default, group chats never reach it, tapbacks and system items never reach it, an empty allowlist means this Mac's own handles, a 30 minute catch-up window means a waking Mac does not replay old texts, and the reply prefix keeps Uplink from answering itself.
  4. State where the data goes, in the same section: the text and the files the agent opens go to Anthropic through the CLI on the user's own plan, no stacksjs server is involved, runs are recorded in a local SQLite database shown only by the local dashboard, the token is in the Keychain, and there is no telemetry and no account.
  5. Rewrite index.stx#L245 so it says what it means. "No server in between" becomes a statement about Uplink adding no server of its own, with the trip to the model provider named rather than left to inference, and it links to the new section.
  6. Reword the bento cell at index.stx#L308-L309 so it is no longer the whole story, and point it at the new section.
  7. Self-host the fonts. Add Geist and Geist Mono woff2 files under public/fonts/, replace index.stx#L104-L106 and 404.stx#L16-L18 with @font-face rules, and drop both preconnects.
  8. Add UPLINK_PERMISSION_MODE to the UPLINK_* block in .env.example, with a one line comment, so the override the new copy names is documented where a from-source user will look for it.

Requirements

  • Do not write that nothing leaves the Mac. The new copy replaces a vague impression with a specific claim, so a sentence like "it all stays on your Mac" would be a regression even though the current page never says it outright. The prompt and the file contents the agent reads go to Anthropic, and the section has to say so.
  • Name the exact strings. bypassPermissions and UPLINK_PERMISSION_MODE, spelled as they appear at app/Uplink/config.ts#L67. A reader who greps the repo for what the page told them has to find it.
  • Write the data-flow sentence so a second engine does not invalidate it. Attribute the trip to the CLI that runs and its provider rather than hardcoding one vendor name into the mechanism sentence, so this section does not need rewriting in the engine track.
  • Keep the Full Disk Access sentence true for both install paths. The claim that only Uplink.app gets the grant holds for the downloadable app and for buddy uplink:install. It does not hold for buddy uplink:watch in a terminal, per README.md#L104.
  • Self-host the same weight axes that are requested today. index.stx#L106 asks for Geist 400..700 and Geist Mono 400..600, and 404.stx#L18 asks for Geist 400..700. Keep font-display: swap and keep the existing fallback stacks at index.stx#L132-L133.
  • Use the tokens and utility classes already defined. u-line, u-raised, u-reveal, u-mono and u-muted at index.stx#L132-L138, and the reveal animation at index.stx#L162-L169. No new colour, no new radius, no client script.
  • No em-dash and no en-dash in the new copy. This is a house rule in AGENTS.md, and neither index.stx nor 404.stx contains one today.

Acceptance

  • The page renders a section between the bento and the download sections, reachable from the nav, and the download section still starts where it did.
  • A reader who has never opened the repo can find on the page: that approvals are off, the literal mode name, the environment variable that changes it, that an unlocked phone with access to the allowed thread is a compromise of the Mac, and that the text and the files the agent opens reach Anthropic.
  • Every factual sentence in the new section maps to a line in the code that says it. A reviewer can walk the list under "What is there today" against the copy and find no claim that is not in app/Uplink/.
  • A grep for fonts.googleapis and fonts.gstatic across resources/views/ returns nothing, both font families load from public/fonts/, and the browser network panel shows no third-party origin on either page.
  • A grep for the em-dash and en-dash characters across resources/views/index.stx and resources/views/404.stx returns nothing.
  • UPLINK_PERMISSION_MODE is present in .env.example.
  • ./buddy lint and ./buddy typecheck pass.

Sign in to comment on this issue.