Cupertino 1.3.0
Version 1.3.0 ·
Two new mutations for Mail (a mailbox, and a rewrite of an unsent draft) and one for Messages (saving an attachment), plus Calendar's first read of the negative space between events. Every server also exposes prompts and resources now, not only tools — see docs/prompts-and-resources.md.
Added
Calendar answers "when am I free".
apple_calendar_find_availabilityreturns the gaps between events, inside working hours, long enough to hold a meeting of a given length. It is notlist_eventswith arithmetic bolted on: it returns the COMPLEMENT of what it read, so every shortfall that merely under-informs a listing instead invents a slot that is already booked. The busy set is complete or there is no answer — a saturated scan, a store that expands no repeating events, and a window past the occurrence cache's edge each returndegraded: truewith a reason rather than a short list. An emptyslotsmeans booked;degradedmeans unknown, and the two are never the same value. Declined and cancelled events do not block time; all-day events do not either, but they are reported next to the slots so a holiday is visible before it is booked over. Working hours default to 09:00–18:00 on weekdays and are set byAPPLE_CALENDAR_WORKDAY_START,APPLE_CALENDAR_WORKDAY_ENDandAPPLE_CALENDAR_WORKDAYS.Messages can save an attachment.
apple_messages_save_attachmentcopies a photo, video, PDF or voice memo out of a conversation. Mail and Notes have had this since 1.0; Messages listed attachments and then left you with no way to get one. Write-gated like the other two, because it puts a file on the user's disk — it sends no Apple Event and changes nothing in Messages, so the claim that writes-off means no Automation grant still holds. Attachments now carry anid(the attachment's guid, never its reusable ROWID) onapple_messages_get_message. An attachment iCloud has offloaded is reported as offloaded rather than as a failure.Every server now exposes prompts and resources, not just tools. Three resources per surface:
cupertino://<surface>/guideis the operating manual and is STATIC, so it still reads on a server whose every grant is denied — which is exactly when it is worth reading;cupertino://<surface>/diagnosticsserves the diagnostics tool's own payload, addressable without spending a call and before the confusing answer rather than after it; andcupertino://<surface>/inventoryholds the accounts, mailboxes, folders, lists or calendars that every other tool takes as an argument. Contacts, Messages and Safari register only the first two — a contact is reached by searching and never by naming its store, chats are unbounded, and history, tabs and the Reading List are three queries rather than three folders. A resource read that fails returnsdegradedDATA rather than a JSON-RPC error, because a protocol error would delete the diagnostics report at the one moment anyone wants it. The scheme iscupertino://and notapple://: a URI scheme is a namespace claim, and taking Apple's would read as the affiliation the README disclaims.Thirteen workflow prompts, one to three per surface. They hold what no tool description can: not what a call does, but what order the calls go in.
apple_mail_triagesearches instead of paging;apple_mail_draft_replyreads the thread first and fills the body, becausereply_to_messagewithout one leaves an EMPTY draft that must never be reported as a written reply;apple_reminders_capture_action_itemssearches before creating, because nothing on that surface deduplicates;apple_calendar_schedulesays out loud that nobody was invited, since this server has noattendeesparameter anywhere on purpose;apple_contacts_who_isrefuses to guess between ambiguous matches;apple_messages_sendconfirms the handle before an action with no undo, and treatsreconciliation: "pending"as SENT rather than as something to retry. Each prompt embeds its surface guide, so a host expanding one hands the model the reference with the task. Write-gated prompts follow the write tools exactly — with writes off they are not refused, they are not registered.Mail can rewrite an unsent draft.
apple_mail_update_draftreplaces a saved draft's body. Mail offers no way to edit one —message.contentis read-only and the dictionary has noopenand noeditcommand — so the tool recreates the draft and deletes the original only once the replacement is confirmed present in Drafts, never the other way round. It refuses rather than doing damage: a reply or forward draft (recreating it would dropIn-Reply-Toand silently start a new thread), a draft carrying attachments (they cannot be re-attached), and anything outside the Drafts mailbox (deleting a sent message and writing a lookalike is not editing). In every refusal the original is untouched. The dictionary evidence behind all of this is now written down in docs/mail-compose.md, including thatoutgoing message.html contentis documented by Apple as "Does nothing at all (deprecated)" — a write-only property that accepts every assignment and does nothing, which is the first thing anyone tries. Two further findings came from probing a live Mail and contradict the dictionary outright:account.draftsMailboxis declared readable and raises "Can't get object." on every account (iCloud, IMAP, Exchange), so the Drafts mailbox is discovered through the unified All Drafts mailbox instead of trusting the documented property; and a freshly saved draft's row id is rewritten by sync within seconds, killing any reference held across it, so the original is refetched by id immediately before deletion.save()itself works and lands in under 400 ms.APPLE_<SURFACE>_EXPOSE_PROMPTSturns the prompts and resources off, defaulting to ON — the opposite ofALLOW_WRITES, and deliberately. That one is a safety invariant; this is a cost knob in the family ofMAX_RESULTS, and modelling it on the write gate would muddy the gate that matters. Measured across all seven servers with writes on, the prompt and resource listings come to ~3.4k tokens against ~18.5k for the tool definitions — about 18% on top of a bill tools dominate either way, with resource contents costing nothing until something reads one. One flag covers both, because a prompt embeds its surface guide and prompts without resources would name acupertino://…/guidethat nothing serves. With it off the capability is never declared, so a client asking gets "method not found" rather than an empty list.diagnosticsreports the flag on every surface, since that tool is still registered when the resources are not.The app shows each surface's live tool, prompt and resource catalog. A Capabilities card on the surface detail pane spawns that surface's server and asks it directly —
initialize,tools/list,prompts/list,resources/list— rather than keeping a hand-written copy that could drift from the servers it describes. Cached per surface AND per write setting, so flipping Allow writes re-probes instead of showing a stale list: that is the point, because this is the only place the app's long-standing claim that writes-off means the mutating tools (and now the prompts that end in one) are never registered rather than refused is actually demonstrated, on screen, live.Mail can create a mailbox.
apple_mail_create_mailboxmakes the folder thatapple_mail_move_messagesneeds to move into — nested with/, or local under On My Mac when no account is named. Calling it for a name that already exists does nothing and says so, so it is safe to call before a move. On a server account it creates the folder ON THE SERVER, which syncs to every device; the tool's description says so and it takesconfirm.
Fixed
The Activity window under-reported what an agent did.
RequestObservernamed the tool behind atools/calland counted it, but letprompts/getandresources/readfall through as bare method names that counted as nothing — so expanding a write workflow likeapple_mail_draft_replylogged less than a single read. Both now log what they reached for, and both count. The footer under the log grew to match ("Tool, prompt and resource names only — never arguments, message contents or results"): it is load-bearing rather than decoration, and the alternative to growing it was leaving it false.The host stopped answering after about 64 sessions, silently.
ServerHostran every blocking part of a session — the two pumps, the stderr drain, and thegroup.wait()that outlives them — on libdispatch's global queues, which are a bounded pool of roughly 64 threads per QoS. A blocked thread is not an available one, so sessions consumed the pool rather than sharing it, and past the limit newly submitted blocks were never scheduled at all. The block that never ran wasserve(client):acceptkept succeeding, so every new bridge completed itsconnect, wrote its handshake, and then waited forever on a reply from a function that had not started. Nothing logged an error, because from the host's side nothing had failed. Measured on a host up three days: 68 threads parked in_dispatch_group_wait_slow,acceptLoophealthy and blocked inaccept, 68 server processes still alive because their pumps had never run to hand them an EOF, 955 open descriptors, and a probe getting zero bytes back in eight seconds from a socket that accepted it in two. Pumps, drains and sessions now each get a thread of their own, and the stderr drain is no longer a member of the group that gates teardown — logging is not plumbing, and a group member that only ends when the child exits makes every teardown hostage to the child agreeing to go.A wedged host produced immortal bridges instead of an error.
cupertino-bridgewaited on the handshake reply with no deadline and nothing watching stdin, so a host that accepted but never answered left the relay alive indefinitely — 67 of them parented to launchd, each still pinning a session's descriptors and threads on the app side, which fed the condition that caused it. The handshake now has a 15sSO_RCVTIMEOand says what it means, and the relay ends when either direction closes: previously only Cupertino hanging up could end it, so a host that quit was never reason enough for the bridge to stop.Four tools returned their payload wrapped twice.
wrap()JSON-encodes whatever its body returns, andapple_messages_get_message,apple_contacts_get_contact,apple_contacts_create_contactandapple_contacts_update_contacthad bodies that returnedok(…)themselves — so the text a client received was a serialised tool result with the real answer one decode further in. The same mistake swallowedfail(): a ref that no longer resolved came back as a SUCCESSFUL call carryingisErroras data. All four now usewrapResult(). Nothing caught it because every assertion read a field off the parsed text and gotundefined, which reads as "absent" rather than "wrapped twice"; there are now tests on the envelope itself.