Commit Graph
12 Commits
Author SHA1 Message Date
james.maes bf5478490b feat(app): proper app icon set + sync helper LSMinimumSystemVersion to 14.0
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)
2026-04-27 17:07:00 -05:00
james.maesandClaude Opus 4.7 bb821d73c0 fix(05-review): CR-04 fixup remove literal LSBackgroundOnly from comment
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>
2026-04-25 16:41:05 -05:00
james.maesandClaude Opus 4.7 0323985786 fix(05-review): WR-05 hop pasteboard reads onto MainActor
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>
2026-04-25 16:31:40 -05:00
james.maesandClaude Opus 4.7 6cbe8a551c fix(05-review): CR-04 remove LSBackgroundOnly from mcp-clipboard
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>
2026-04-25 16:21:47 -05:00
james.maes 45328e3871 feat(05-03): bidirectional per-helper entitlement enforcement + 3 fixtures
- 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
2026-04-25 13:34:29 -05:00
james.maes 618aa73f4b feat(05-03): xcodegen wiring + Copy Helpers loop extension for mcp-applescript
- project.yml: register mcp-applescript target mirroring mcp-time/clipboard
  shape with CODE_SIGN_ENTITLEMENTS=Support/mcp-applescript.entitlements
- Jarvis target gains mcp-applescript dependency (embed=false link=false)
- Copy MCP helpers loop extends mcp-time mcp-clipboard mcp-applescript
- Per-helper postBuildScript copies Support/mcp-applescript.entitlements into
  bundle Contents/Resources/ where codesign.sh's deepest-first walker expects
- Strip XML comment from entitlements file: AMFIUnserializeXML rejects plist
  comments at line 6, while plutil -lint accepts them. Other helpers' .entitlements
  also have no comments — matching the pattern that already works.
- Debug build: Jarvis.app/Contents/Helpers/{mcp-time,mcp-clipboard,mcp-applescript}.app
  all sign cleanly; codesign -d --entitlements shows apple-events=true ONLY on
  mcp-applescript (mcp-time=0, mcp-clipboard=0, mcp-applescript=1)
- xcodegen generate updates project.pbxproj; no CodeSignOnCopy = YES introduced
2026-04-25 13:22:22 -05:00
james.maes 2c75d94ed3 feat(05-03): GREEN — mcp-applescript helper with run_applescript tool
- AppleScriptRunner: in-process NSAppleScript wrapper (NEVER osascript subprocess
  — that splits TCC identity per T-05-03-03)
- AppleScriptOutcome enum: .success / .compileFailure / .runtimeError(num, msg)
- Empty/whitespace source explicitly rejected as .compileFailure
- OSA error-number range -2700..-2799 distinguishes compile from runtime errors
- MCPAppleScript main: registers run_applescript with single 'source' string arg
- Scaffold-time stderr probe runs trivial script at init (RESEARCH OQ#3 / T-05-03-05)
- Info.plist: LSUIElement+LSBackgroundOnly+CFBundleId+NSAppleEventsUsageDescription
- mcp-applescript.entitlements: ONLY com.apple.security.automation.apple-events=true
- 4/4 AppleScriptRunner tests pass (locally; XCTSkip via JARVIS_SKIP_NSAPPLESCRIPT)
- swift build -c release produces working executable

Closes MCP-04 (helper portion). Confirmation broker + dispatcher gating in 05-04/05-05.
2026-04-25 13:17:50 -05:00
james.maes 4510644bcd test(05-03): RED — AppleScriptRunner test scaffold for run_applescript
- 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)
2026-04-25 13:13:18 -05:00
james.maes c9a58b42e2 feat(05-02): xcodegen wiring + Copy Helpers script + integration tests
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
2026-04-25 13:04:15 -05:00
james.maes 69e5021ebe feat(05-02): GREEN — mcp-clipboard helper with MCP-03 fileURL refusal
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
2026-04-25 11:24:00 -05:00
james.maes b9eae47925 test(05-02): RED — PasteboardReader test scaffold for MCP-03 refusal
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.
2026-04-25 11:19:49 -05:00
james.maes 06bae38cc9 feat(05-02): mcp-time helper — get_time tool returns ISO 8601
- SPM executable target depending on swift-sdk MCP @ 0.12.0
- Server registers single 'get_time' tool; CallTool returns
  ISO8601DateFormatter().string(Date()) with fractional seconds
- Info.plist sets LSUIElement=YES + LSBackgroundOnly=YES (Pitfall #3)
  + CFBundleIdentifier=com.koftwentytwo.jarvis.mcp-time
- mcp-time.entitlements is empty <dict/> — Hardened Runtime baseline
  only, NO automation.apple-events (only mcp-applescript holds that)
- Default StdioTransport for helper side; logs go to stderr via SDK
  default Logger (no print() calls would corrupt JSON-RPC stdout)

MCP-02 closes after Task 3 wires the helper into Jarvis.app/Contents/Helpers.
2026-04-25 11:18:01 -05:00