Cupertino 1.5.0
Version 1.5.0 ·
Added
Safari can read what a page actually says.
apple_safari_read_pagereturns a tab's readable text or its raw HTML — the capability this surface has never had and the one most often expected of it. It arrives through a Safari web extension rather than Apple Events, so it needs no Full Disk Access and no Automation grant. Verified end to end against a real capture rather than only in tests: Hacker News, 3,681 characters of text, through the built server over stdio, on a run that reportedhistory=UNREADABLEin the same breath. The three Safari lanes are independent, and this is the one that needs no grant at all.macOS does offer a built-in route — "Allow JavaScript from Apple Events" — and it is a skeleton key: one switch, no scope, and afterwards any process that can send Apple Events runs script in any tab, including whatever is logged in. Its state cannot even be read back, so nothing can tell you it is on. The extension is asked per website instead.
The hand-off is a file, because nothing can push into a running MCP server:
ServerHostspawns node with a fixed environment and there is no channel afterwards. The extension writes captures into its own appex container, which is 0700, owned by the user and not TCC-protected, so the unsandboxed server reads it with no grant and no app group — measured with a negative control, a shell denied onHistory.db,~/Library/Mailandchat.dbstill reads three appex containers. It is a cache and says so: a 30 minute TTL, 20 entries, 256 KB of text.Settings grows a row under Page content saying whether the extension is switched on, with a button into Safari, where that switch actually lives — there is no system preferences pane for it. Only the containing app can ask: a standalone binary calling the same API is refused with
SFErrorDomainerror 1. A disabled extension leaves its last captures on disk and simply stops adding more, so without this the store keeps looking healthy while answering with an ever-older page.The Activity log records what a call was made with, not just its name. A per-surface capture mode chooses between names only, arguments, and arguments with results. Content is blanked by default, and separately: a mail body, a message and a note's text all arrive as arguments, so the prose inside them is dropped while the structure — which tool, which mailbox, which recipient — is kept. Nothing is written to disk; the log stays a bounded in-memory ring.
Messages can read one-time codes, behind
APPLE_MESSAGES_ALLOW_CODES.find_codesis a read, and the reason it needs a switch of its own rather than riding onallowWritesis what it completes: this server already holds the conversation history while a sibling holds Mail, which between them is the password-reset channel. Adding live authentication codes to that assembles an account-takeover primitive out of parts that were each individually reasonable, so it defaults off. It ships in place of a Passwords surface, which does not exist.Safari tells the frontmost tab from the merely active one.
activeis selected in its own window, so two open windows produced two active tabs and nothing said which one a person was actually looking at.apple_safari_list_tabsnow also reads Safari's ownfrontmostproperty and each window's front-to-back index, marks at most one tabfrontmost, and takesonly: "active" | "frontmost"to narrow the result. When that order cannot be read it sayswindowOrderUnknown, so a caller asking for it gets an explanation rather than a silent empty list.The history enrichment behind it was rebuilt at the same time. Safari shares no id between the live-tab and history lanes, so the join is a URL and nothing else, and the old one was a single exact lookup plus a query-stripping retry run as up to 2N individual reads. An ordered ladder now tries fragment, trailing slash, scheme,
www.and tracking parameters before falling back to that query strip as the weakest rung, resolves a whole tab set in one chunked SQLIN, and reports which rung answered so a caller can tell a faithful match from a loose one.Surfaces can be switched off, in Settings › Surfaces. A surface that is off is not served at all: it is left out of the server keys Cupertino writes into a client's config, pruned from the configs it has already written, and refused at the bridge if an older config still asks for it.
The cost this removes is the one
ProjectScopealready measured — eight servers wired into every client means every session carries tool definitions for surfaces nobody uses. Turning one off is how you stop paying for the ones you do not want, without giving up the ones you do.Switching a surface off also stops its running servers with
SIGTERM. An MCP host opens one stdio connection when the editor launches and keeps it for the life of that editor, so refusing new connections alone would have left the tools sitting in a session that outlived the decision.Clients configured before the change keep the entry until you press Update, and the Clients pane now says which ones still hold it. Claude Code and Codex get a copyable removal command; Visual Studio Code has no command that removes a server, so that row explains the manual edit instead.
Absence means enabled, so nothing changes for an existing install and a surface added in a later version still arrives switched on.
Changed
Every per-surface control now lives in the main window, and Settings keeps only what has no surface. Automation and the write toggles left the Permissions pane for the surface detail pane, which already carried both. Permissions is now Full Disk Access, Accessibility and System Events — the three grants that cannot be expressed per app.
The main window has had one pane per surface for a while, and that pane's own note records these facts having once been "scattered between the popover, the Permissions tab and the log filter" with nothing answering "is Mail working" in one place. A second copy in Settings was that scattering, still going.
The sidebar dims a switched-off surface rather than hiding it, and drops its automation glyph — a TCC grant reported against a server that will never start is a true fact about the wrong subject. Right-clicking a row turns it off without leaving the list. The detail pane replaces Access and Capabilities with a single card saying what is off; Capabilities in particular no longer spawns the server, which is the one thing a switched-off surface must not do.
Screenshot runs no longer resize the developer's own windows.
HostedWindownamed a frame autosave even under screenshot mode, so everymake screenshotswrote the capture's pinned size back intoUserDefaultsunder the key the real window reads. Found as a Settings window remembered at 1000x772 against a main window at 1120x572 —DemoSeed's two pinned sizes plus a titlebar, exactly — which is a Settings window 200pt taller than the one it opens in front of.Closing that leak exposed what it had been hiding: the main window passed no
contentSizeat all, and its captures had been relying on the leaked frame to come out at 1120x540 rather than SwiftUI's 780x492 fitting size. It now pins its own, the way the Settings window already did.Both windows also set
tabbingMode = .disallowed. Neither is a document, and on a Mac set to "Prefer tabs when opening documents: Always" macOS was free to absorb Settings as a tab of the main window.The Settings window opens a little smaller, at 720x520, so the main window's sidebar stays visible behind it.
The two Settings App Store plates were re-aimed.
settingsnow photographs the Clients pane, which nothing showed before;writesis the main window on a surface with writes off, where the capabilities card reports seven tools against Mail's twenty. The write gate used to be pictured as a row of toggles, which showed the control — this shows the consequence, and the numbers come off the real server rather than a fixture.The menu bar glyph is a setting sun: a solid disc clipped by a stroked horizon, replacing the thin-wave mark it had before. The cut is a
clipPathrather than the canvas's own mask — measured throughNSImage's SVG rep, the mask's soft edge leaves about 25% residual alpha along the cut and bridges the disc and the horizon into one shape. The connected state adds a halo whose gap was swept rather than chosen: 2.4 units is one step past where the ring's antialiased edge stops fusing with the disc at 2x.The website gained a Safari section, a privacy policy, a feedback form, and the EULA at
/terms. The Safari section is about the permission rather than the feature, which is the same argument the rest of the site already makes about Full Disk Access and terminals: reading a web page is unremarkable, and what you hand over in order to do it is not. The EULA is rendered fromapps/apple/EULArather than restated, so the document a buyer agrees to at checkout is the one the app was built with. The feedback form exists for the reports that should not be public — a useful Cupertino bug report quotes a subject line, a chat, an account name or a contact, and the issue tracker is public and permanent. Organization and WebSite JSON-LD were added alongside.The menu bar popover groups its buttons by what they do: "Open Cupertino" sits left, and "Settings…" moves right to join "Quit". What opens something is on one side, what leaves is on the other, and Settings belongs with the second — it is a window you go to, not a thing the panel itself does.
Fixed
The Activity log pane rendered nothing at all. Its footer caption grew a second sentence when arguments started being recorded, and it carried
fixedSize(horizontal: false, vertical: true)— a request for whatever height the text needs at the width it is proposed. Inside thatHStackthe proposed width is near zero, so the caption wrapped into a column 2060pt tall, the split view adopted that as its ideal height, and a 572pt window laid its entire contents out at y = -587. Sidebar, log and footer all existed in the accessibility tree and none of them was on screen.The ask is now bounded with
lineLimit(3)instead — three, not the two the capture needs, because this window goes down to 780pt wide and truncating a privacy claim with an ellipsis is the one way this footer must not fail.layoutPriorityand a flexibleframewere both tried and neither reaches it — the fix is not to ask for an unbounded height in the first place. It was latent under the one-line caption, which wrapped tall enough to be wrong and short enough to fit.Found by the screenshot pipeline: the
activityandpromptplates came out as empty windows, and because both were empty they were also byte-identical, whichappshot check's duplicate detection reports as a staging failure rather than a visual change.Settings no longer opens as a tab of the main window.
NSWindow.tabbingModedefaults to.automatic, so on a Mac with Desktop & Dock → "Prefer tabs when opening documents" set to Always, macOS tabs any two same-class titled, resizable windows together. Neither of this app's windows is a document and neither has a second instance to be tabbed with, so Settings was absorbed as a tab of the main window and ⌘, appeared to do nothing — the pane it opened was behind the tab already showing.Maps is no longer described as read-only. The Permissions pane derived its Automation caption from
usesAppleEventsalone, where false meant "not needed — this surface reads only". That held for every surface until Maps, which is the only one withusesAppleEventsfalse that writes anyway, through SQL into its Core Data store rather than an Apple Event. So the one surface that reached the false branch was the one the sentence was wrong about, and 1.4.0 shipped it next to a working write toggle. It branches onsupportsWritesnow; the grant is equally not needed either way, and only the reason differs.A green Accessibility row that was not actually working. This shipped in the 1.4.0 build and is recorded here because it was never announced. One bundle identifier can hold several Accessibility entries at once — one per path and signature it has been granted at, so an installed copy, a debug build and every earlier reinstall each get their own — and the app's
AXIsProcessTrusted()check and the servers' functional check can match different ones.tccutil resetreported clearing four entries forio.mgcrea.cupertinoon the machine that surfaced it. The cure is clearing them and granting once from the running bundle, then never granting again. Diagnostics, the Settings hint and the code comments now say that instead of the retracted claim that Accessibility simply does not inherit.