App icon was missing — AppIcon.appiconset/Contents.json declared 10 size slots
but no PNG files filled in, so Jarvis showed the generic blank-app placeholder
in Dock/Finder/cmd-tab. This commit fills all 10 slots with a generated icon
derived from the existing menu-bar arc-reactor PDF, plus consolidates the
helper Info.plists onto the same MACOSX_DEPLOYMENT_TARGET pattern the main
target already uses.
Icon generation
---------------
- Render Icon-MenuBar-Template.pdf at 1024×1024 via qlmanage
- Composite via scripts/build-app-icon.swift (Core Graphics):
- Squircle clip (radius = size × 0.2237 per Apple HIG)
- Radial gradient backplate: #4FC3F7 cyan core → #0D47A1 navy → #061229
near-black (Iron Man arc-reactor palette)
- Inner glow ring at 35% radius for energized feel
- Silhouette as luminosity mask, filled near-white with cyan tint
- 2.5px cyan rim for definition against any wallpaper
- Resize to 7 unique pixel sizes (16/32/64/128/256/512/1024) via sips
- Map into the 10 AppIcon.appiconset slots (each idiom×scale combination
gets its own filename for clarity, even though sizes overlap)
- AppIcon.icns is now produced by Xcode's actool at 52KB
Helper deployment-target sync
-----------------------------
The macOS 14 deployment-target bump in 722e25b (Phase 6 close-out, Voice
package's ORT 1.24.2 floor) updated the main Jarvis target via project.yml
but missed the three MCP helper Info.plists, which still hardcoded
LSMinimumSystemVersion=13.0. xcodebuild auto-corrected at build time but
emitted a warning per helper. Update each to use $(MACOSX_DEPLOYMENT_TARGET)
so future deployment-target changes propagate automatically (matches the
pattern the main App/Info.plist already uses).
Files
-----
- App/Assets.xcassets/AppIcon.appiconset/Contents.json — slot filenames
- App/Assets.xcassets/AppIcon.appiconset/icon_*.png — 10 icon PNGs
- scripts/build-app-icon.swift — reproducible icon-generation script
- mcp-servers/mcp-{clipboard,applescript,time}/Support/Info.plist — sync to $(MACOSX_DEPLOYMENT_TARGET)
Verification
------------
- xcodebuild Debug build: BUILD SUCCEEDED, zero LSMinimumSystem warnings
- AppIcon.icns present in Jarvis.app/Contents/Resources/ (52KB)
The CR-04 verification gate `! grep -q 'LSBackgroundOnly' Info.plist`
treats the literal string as forbidden anywhere in the file, including
in comments. Replaced the literal mentions in the explanatory comment
with descriptive prose ("background-only LaunchServices key", "background-
only daemons") that conveys the same meaning without the literal that
the verification gate inspects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
macOS Sequoia/Sonoma require main-thread access for NSPasteboard.general
reads in some configurations; background-thread reads can return stale
or empty data with no error. SystemPasteboard now hops to MainActor
via MainActor.assumeIsolated (fast path when already on main) or
DispatchQueue.main.sync (when called from a background queue, e.g.,
the SDK's CallTool handler executor). Compounds with CR-04
(LSBackgroundOnly removal) — both addressed the same silent-empty
failure mode from different angles.
CR-04's seeded-pasteboard integration test now serves as the runtime
guard for WR-05 too: any regression that returns empty pasteboard data
will surface as a failed assertion on the seeded UUID string.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
LSBackgroundOnly: true silently denied NSPasteboard.general access on
macOS Sonoma+ (Apple feedback FB13402281 lineage). LSUIElement: true
already suppressed the Dock icon — both keys together were redundant
and one was actively breaking pasteboard reads. Removed
LSBackgroundOnly per Option A in REVIEW 05; LSUIElement stays.
Adds test_get_clipboard_seeded_returnsSeededString_CR04 that seeds
NSPasteboard.general with a UUID-tagged string in the parent test
process, then asserts the helper's response is EXACTLY that string
(not the silent-empty marker that the pre-fix LSBackgroundOnly
configuration produced). Skips on headless CI where the parent
can't write the pasteboard.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- verify-entitlements.sh: refactor inline mcp-applescript helper grep into
check_helper_entitlements_xml() driven off per-helper required+forbidden
arrays. mcp-applescript MUST carry apple-events; mcp-time/mcp-clipboard
MUST NOT (and forbidden list also rejects allow-jit / speech-recognition-
assets / audio-input / allow-unsigned-executable-memory drift onto helpers)
- Unknown-helper case is a fatal error — 'no default-allow branch' so authoring
drift cannot smuggle in an unconfigured helper
- Empty array guard via ${#array[@]} prevents bash 'unbound variable' under
set -u when a helper has no required keys (Rule 1 fix discovered via Debug
xcodebuild)
- New --verify-helper-fixture <plist> <helper-name> mode for fault-injection
harness — feeds a fixture file in and runs check_helper_entitlements_xml
without needing a real signed bundle
- 3 new fixtures cover the three threat-model arms:
helper-applescript-good T-pos: mcp-applescript with apple-events
helper-applescript-missing T-05-03-02: mcp-applescript MISSING apple-events
helper-time-with-apple-events T-05-03-01: cross-helper drift (forbidden)
- test-verify-entitlements.sh: add run_helper_case() driver + 3 invocations,
total fixture corpus now 6 cases (3 P1-05 + 3 P5-03)
- AppleScriptRunner.swift / MCPAppleScriptMain.swift: reword TCC-isolation
comments so the literal subprocess CLI binary name does not appear in source
(plan's anti-pattern grep '! grep -q osascript' enforces absence)
- Debug xcodebuild end-to-end: post-codesign walker reports
'helper mcp-time / mcp-clipboard / mcp-applescript entitlements OK' x3
- Fault-injection harness: PASS=6 FAIL=0
- New mcp-servers/mcp-applescript SPM package skeleton (Package.swift)
- AppleScriptRunnerTests covers four behaviors: success/.compileFailure/.runtimeError/empty
- Tests opt-out via JARVIS_SKIP_NSAPPLESCRIPT=1 for headless CI
- Fails at compile time: 'no such module mcp_applescript' (no executable target source yet)
project.yml:
- Register swift-sdk @ 0.12.0 as a top-level package (consumed by both
helper targets; same exact version as packages/MCP/Package.swift)
- Add mcp-time and mcp-clipboard application targets with per-config
signing (Debug=ad-hoc CODE_SIGNING_ALLOWED=NO, Release=hardened
runtime + --options=runtime --timestamp). Each target carries its
own CODE_SIGN_ENTITLEMENTS pointing at mcp-servers/<helper>/Support/
- Each helper has a postBuildScript that copies its source
.entitlements into <bundle>/Contents/Resources/ where codesign.sh
expects it (per P1-05 contract)
- Jarvis target depends on both helpers (embed=false, link=false —
helpers are independent .app subprocesses, not linked frameworks)
DEVIATION (Rule 3, blocking issue): xcodegen 'copyFiles' phase doesn't
resolve target products from BUILT_PRODUCTS_DIR — it expects file
paths relative to SRCROOT. The plan's
'files: [mcp-time.app, mcp-clipboard.app]' produced an empty
Contents/Helpers/ at build time. Switched to a postBuildScript on
Jarvis ('Copy MCP helpers into Contents/Helpers/') that 'cp -R' from
BUILT_PRODUCTS_DIR. This matches the plan's deviation_rules clause #1
('If xcodegen doesn't directly support nested .app bundles via
Contents/Helpers/<Name>.app/, generate the helper bundles via
separate xcodebuild targets and copy them in via a Build Phase
script.'). Code Sign On Copy stays disabled — codesign.sh's
deepest-first walker is the authoritative signer.
DEVIATION (Rule 1, bug): mcp-time/Sources/mcp-time/main.swift renamed
to MCPTimeMain.swift to fix Xcode's '@main attribute cannot be used
in a module that contains top-level code' error. Same root cause as
mcp-clipboard's earlier rename — when the file is named main.swift,
Xcode treats it as a script and rejects @main on a struct.
Standalone 'swift build' tolerated it; xcodebuild does not.
Integration tests (packages/MCP/Tests/MCPTests/):
- HelperBundleLocator.swift: shared resolver, env override →
Bundle.main walk → DerivedData walk (Release-preferred; skips Debug
builds with adjacent <helper>.debug.dylib because Xcode 26's
incremental-link artifact has a code signature mismatch with the
parent and dyld rejects on subprocess launch)
- MCPTimeIntegrationTests: round-trip get_time, parse via
ISO8601DateFormatter, assert ±60s of Date()
- MCPClipboardIntegrationTests: round-trip get_clipboard, accept any
of (text/empty/refusal) outcomes since the test runner's pasteboard
state is unknown
DEVIATION (Rule 1, bug): Both integration tests use synchronous
'await client.shutdown()' rather than 'defer { Task { await ... } }'.
Fire-and-forget shutdown leaks helper stdio FDs into the next test;
ChildSpawnGate's DEBUG-only fatalError precondition (FD must be
CLOEXEC) crashes the next spawn. Discovered when MCPRestartTests ran
after the integration tests in the same suite.
Verification:
- swift test --package-path packages/MCP: 18/18 pass (16 from 05-01
+ 2 new integration). FD leak baseline=4 final=4 delta=0
- swift test --package-path mcp-servers/mcp-clipboard: 6/6 pass
- xcodegen generate: clean
- xcodebuild Release build: SUCCEEDED end-to-end (codesign + verify)
- Both helper bundles signed with empty entitlements; 0 apple-events
- 0 CodeSignOnCopy=YES, 0 --deep in pbxproj
PasteboardReader.swift (testable):
- PasteboardLike protocol abstracts NSPasteboard for tests
- ClipboardReadResult (.text/.empty/.refusedFileURL), Sendable + Equatable
- read() refuses fileURL pasteboards BEFORE attempting string extraction
(T-05-02-01: prevents model coercion via fileURL+string composite)
MCPClipboardMain.swift (renamed from main.swift to allow @main):
- @main on a file named main.swift conflicts with SPM's auto top-level
treatment; renamed to MCPClipboardMain.swift (deviation Rule 1)
- SystemPasteboard adapter is @unchecked Sendable; reads via
NSPasteboard.general singleton inside each method (no stored ref to
the non-Sendable NSPasteboard class) — addresses Swift 6 strict
concurrency requirement on the PasteboardLike protocol's Sendable bound
- get_clipboard CallTool handler returns:
- isError=true 'refusing file URL' on .refusedFileURL
- isError=false '(clipboard empty)' on .empty
- isError=false <text> on .text(s)
Info.plist: LSUIElement=YES + LSBackgroundOnly=YES, bundle id
com.koftwentytwo.jarvis.mcp-clipboard. Entitlements is empty <dict/> —
NO automation.apple-events.
All 6 PasteboardReaderTests pass; release build produces runnable binary.
Deviations (Rule 1 fixes):
- Renamed main.swift -> MCPClipboardMain.swift to resolve @main conflict
- SystemPasteboard moved from stored-NSPasteboard to singleton-access to
satisfy Sendable conformance under SWIFT_STRICT_CONCURRENCY=complete
Six unit tests covering the refusal matrix:
- fileURL + string both present → .refusedFileURL (model-coercion vector)
- fileURL only → .refusedFileURL
- string only (no fileURL) → .text(content)
- empty string → .empty
- nil string + non-fileURL type → .empty
- nil types → .empty
Tests use a MockPasteboard conforming to a PasteboardLike protocol so the
reader is testable without touching NSPasteboard.general (which would
require a GUI session and pollute test runs).
Build fails because PasteboardReader + PasteboardLike + ClipboardReadResult
do not yet exist — GREEN step adds them next.