diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ee972124..ac98b5e1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -92,6 +92,7 @@ jobs: - name: Build release binaries run: | + # mootx01-daemon is wrapped as Mootx01DaemonProvider.app by the pkg step. # mootx01 = MCP server + management CLI; moot-mgr = the management # console (resident host + loopback dashboard). The macOS moot-mgr is a # SwiftUI app (built here and on x86_64); Linux and Windows ship the @@ -100,6 +101,7 @@ jobs: # SPM dependencies are public github.com packages and build without # auth injection (verified: candidate.yml omits this step and builds fine). swift build -c release --package-path apps/mootx01 --product mootx01 + swift build -c release --package-path apps/mootx01 --product mootx01-daemon swift build -c release --package-path apps/moot-mgr --product moot-mgr - name: Import Developer ID Certificate @@ -142,6 +144,7 @@ jobs: codesign --verify --deep --strict --verbose=2 "$MGR_BIN" echo "MOOT_BIN=$MOOT_BIN" >> "$GITHUB_ENV" echo "MGR_BIN=$MGR_BIN" >> "$GITHUB_ENV" + echo "DAEMON_BIN=apps/mootx01/.build/arm64-apple-macosx/release/mootx01-daemon" >> "$GITHUB_ENV" - name: Notarize binaries if: ${{ startsWith(github.ref, 'refs/tags/') && github.event_name == 'push' }} @@ -266,9 +269,16 @@ jobs: # Application identity seals the bundled Mootx01Setup.app (hardened # runtime) inside the .pkg so the package passes notarization. APP_IDENTITY: "Developer ID Application: Codedaptive, LLC (${{ secrets.APPLE_TEAM_ID }})" + DAEMON_PROFILE_BASE64: ${{ secrets.APPLE_DAEMON_PROVISIONING_PROFILE }} # Fail closed: a published .pkg must be signed (SECURITY 927f38c4). REQUIRE_SIGNING: "1" run: | + if [ -z "$DAEMON_PROFILE_BASE64" ]; then + echo "Daemon provider provisioning profile secret is missing" >&2 + exit 1 + fi + export DAEMON_PROVISIONING_PROFILE="$RUNNER_TEMP/daemon-provider.provisionprofile" + echo "$DAEMON_PROFILE_BASE64" | base64 --decode > "$DAEMON_PROVISIONING_PROFILE" # Strip leading 'v' from tag (build-pkg.sh uses version in filename). VERSION_NUM="${VERSION#v}" bash distribution/macos/build-pkg.sh \ @@ -276,7 +286,8 @@ jobs: arm64 \ "${MOOT_BIN}" \ "${MGR_BIN}" \ - "${SETUP_BIN}" + "${SETUP_BIN}" \ + "${DAEMON_BIN}" PKG_ASSET="mootx01-${VERSION_NUM}-macos-arm64.pkg" echo "PKG_ASSET=${PKG_ASSET}" >> "${GITHUB_ENV}" @@ -345,9 +356,11 @@ jobs: - name: Build release binaries (cross-compile to x86_64) run: | + # mootx01-daemon is wrapped as Mootx01DaemonProvider.app by the pkg step. # Both binaries cross-compiled to x86_64; see the arm64 job for the # mootx01/moot-mgr split rationale (macOS moot-mgr = SwiftUI app). swift build -c release --package-path apps/mootx01 --product mootx01 --arch x86_64 + swift build -c release --package-path apps/mootx01 --product mootx01-daemon --arch x86_64 swift build -c release --package-path apps/moot-mgr --product moot-mgr --arch x86_64 - name: Import Developer ID Certificate @@ -390,6 +403,7 @@ jobs: codesign --verify --deep --strict --verbose=2 "$MGR_BIN" echo "MOOT_BIN=$MOOT_BIN" >> "$GITHUB_ENV" echo "MGR_BIN=$MGR_BIN" >> "$GITHUB_ENV" + echo "DAEMON_BIN=apps/mootx01/.build/x86_64-apple-macosx/release/mootx01-daemon" >> "$GITHUB_ENV" - name: Notarize binaries if: ${{ startsWith(github.ref, 'refs/tags/') && github.event_name == 'push' }} @@ -491,16 +505,24 @@ jobs: # Application identity seals the bundled Mootx01Setup.app (hardened # runtime) inside the .pkg so the package passes notarization. APP_IDENTITY: "Developer ID Application: Codedaptive, LLC (${{ secrets.APPLE_TEAM_ID }})" + DAEMON_PROFILE_BASE64: ${{ secrets.APPLE_DAEMON_PROVISIONING_PROFILE }} # Fail closed: a published .pkg must be signed (SECURITY 927f38c4). REQUIRE_SIGNING: "1" run: | + if [ -z "$DAEMON_PROFILE_BASE64" ]; then + echo "Daemon provider provisioning profile secret is missing" >&2 + exit 1 + fi + export DAEMON_PROVISIONING_PROFILE="$RUNNER_TEMP/daemon-provider.provisionprofile" + echo "$DAEMON_PROFILE_BASE64" | base64 --decode > "$DAEMON_PROVISIONING_PROFILE" VERSION_NUM="${VERSION#v}" bash distribution/macos/build-pkg.sh \ "${VERSION_NUM}" \ x86_64 \ "${MOOT_BIN}" \ "${MGR_BIN}" \ - "${SETUP_BIN}" + "${SETUP_BIN}" \ + "${DAEMON_BIN}" PKG_ASSET="mootx01-${VERSION_NUM}-macos-x86_64.pkg" echo "PKG_ASSET=${PKG_ASSET}" >> "${GITHUB_ENV}" diff --git a/Makefile b/Makefile index 48c837f78..acab3e408 100644 --- a/Makefile +++ b/Makefile @@ -288,17 +288,21 @@ release: # and INSTALLER_IDENTITY are exported (build-pkg.sh warns and proceeds — # fine for local layout testing, not distributable). Version defaults to # the newest CHANGELOG.md entry; override with make pkg PKG_VERSION=X.Y.Z. +# The release package carries `Mootx01DaemonProvider.app` alongside the CLI and +# setup assistant; build-pkg.sh refuses a signed release without it. PKG_VERSION ?= $(shell sed -n 's/^\#\# v\([^ ]*\) .*/\1/p' CHANGELOG.md | head -1) pkg: @mkdir -p "$(DIST)" swift build -c release --package-path apps/mootx01 --product mootx01 + swift build -c release --package-path apps/mootx01 --product mootx01-daemon swift build -c release --package-path apps/moot-mgr --product moot-mgr swift build -c release --package-path apps/Mootx01-Setup --product Mootx01Setup @arch=$$(uname -m); \ distribution/macos/build-pkg.sh "$(PKG_VERSION)" "$$arch" \ apps/mootx01/.build/release/mootx01 \ apps/moot-mgr/.build/release/moot-mgr \ - apps/Mootx01-Setup/.build/release/Mootx01Setup && \ + apps/Mootx01-Setup/.build/release/Mootx01Setup \ + apps/mootx01/.build/release/mootx01-daemon && \ mv "mootx01-$(PKG_VERSION)-macos-$$arch.pkg" "$(DIST)/" && \ echo "✓ .pkg written to $(DIST)/mootx01-$(PKG_VERSION)-macos-$$arch.pkg" diff --git a/apps/Mootx01-App/App/Mootx01-iOS.entitlements b/apps/Mootx01-App/App/Mootx01-iOS.entitlements deleted file mode 100644 index a94e8b004..000000000 --- a/apps/Mootx01-App/App/Mootx01-iOS.entitlements +++ /dev/null @@ -1,18 +0,0 @@ - - - - - com.apple.developer.icloud-container-identifiers - - iCloud.com.codedaptive.mootx01 - - com.apple.developer.icloud-services - - CloudKit - - com.apple.security.application-groups - - group.com.codedaptive.mootx01 - - - diff --git a/apps/Mootx01-App/App/Mootx01App.swift b/apps/Mootx01-App/App/Mootx01App.swift deleted file mode 100644 index 9a1356463..000000000 --- a/apps/Mootx01-App/App/Mootx01App.swift +++ /dev/null @@ -1,287 +0,0 @@ -import AppIntents -import GatewayUI -import MootGateway // MinerRunLoop + GatewayRuntime (M-ING-2 executor) -#if os(macOS) -import AppKit -#elseif os(iOS) -import BackgroundTasks -#endif - -// MARK: - Mootx01App -// -// The MOOTx01 ecosystem app — the Apple presentation layer of the app/engine boundary. One -// codebase, two app targets (macOS + iOS/iPadOS), sharing the GatewayUI -// surface. Every platform runs the engine "server-in-app" (embedded); macOS -// adds the app-managed-daemon panel (Engine tab). The clean server binary is -// separate and untouched. -// -// Shortcuts registration: `Mootx01Shortcuts.updateAppShortcutParameters()` is -// called once at every app launch. The App Intents metadata extractor handles -// static phrase registration at build time (via the Xcode app bundle); this -// runtime call refreshes the donated phrases and surfaces them in the Shortcuts -// app and Siri. Without it the phrases registered at build time may go stale -// when content changes, so calling it here keeps them current. -// -// URL scheme (A5): `mootx01://x-callback-url/?…` is declared in -// project.yml → CFBundleURLTypes. The `onOpenURL` modifier on ContentView -// (below) routes inbound URLs through MootURLRouter. - -@main -struct Mootx01App: App { - #if os(macOS) - @NSApplicationDelegateAdaptor(MacAppDelegate.self) private var delegate - #elseif os(iOS) - // APNs push accelerator (CVK-ICLOUD P5-M2): iOS delegate receives remote - // notification callbacks that SwiftUI's App protocol does not expose. - @UIApplicationDelegateAdaptor(IOSAppDelegate.self) private var iosDelegate - #endif - @Environment(\.scenePhase) private var scenePhase - @State private var model = AppModel() - - // M-MXA-7: menu-bar headless mode is a user setting (default ON — the - // app is the macOS mining executor per ruling D9 and must survive its - // last window closing). The same flag drives menu-bar item insertion - // here and the termination policy in MacAppDelegate. - #if os(macOS) - @AppStorage(MenuBarPolicy.defaultsKey) private var menuBarModeEnabled = true - #endif - - init() { - #if DEBUG - EstateConfigurationResolver.installDebugLaunchOverride() - #endif - GatewayRuntime.installIntentProvider() - #if os(iOS) - IOSMiningBackgroundTasks.register() - Task { await IOSMiningBackgroundTasks.schedule() } - #endif - } - - var body: some Scene { - WindowGroup(id: "main") { - #if os(macOS) - ContentView(model: model) - .frame(minWidth: 900, minHeight: 600) - .task { await model.start() } - .task { Mootx01Shortcuts.updateAppShortcutParameters() } - .task { await ShareInboxDrain.drainNow() } - .task { await WidgetSnapshotRefresher.refreshNow() } - .task { - // FAB5-SM: migrate WB2 key → master gate once, then configure. - // migrateIfNeeded() is a no-op on subsequent launches when the - // master key is already present. configure() is idempotent. - SyncPolicy.migrateIfNeeded() - await MootSyncDriver.shared.configure(SyncPolicy.config(enabled: SyncPolicy.isEnabled())) - await MootSyncDriver.shared.syncNow() - } - #else - ContentView(model: model) - .task { await model.start() } - .task { Mootx01Shortcuts.updateAppShortcutParameters() } - .task { await ShareInboxDrain.drainNow() } - .task { await WidgetSnapshotRefresher.refreshNow() } - .task { - // FAB5-SM: migrate WB2 key → master gate once, then configure. - SyncPolicy.migrateIfNeeded() - await MootSyncDriver.shared.configure(SyncPolicy.config(enabled: SyncPolicy.isEnabled())) - await MootSyncDriver.shared.syncNow() - } - // A4b: content shared while the app was backgrounded drains - // on the next foregrounding, not only at launch; the widget - // projection and CloudKit sync run on the same beat. - .onChange(of: scenePhase) { _, phase in - guard phase == .active else { return } - Task { - await ShareInboxDrain.drainNow() - await WidgetSnapshotRefresher.refreshNow() - await MootSyncDriver.shared.syncNow() - } - } - #endif - } - - #if os(macOS) - // FAB5-SM: system Settings window (Cmd+,). SettingsView owns the master - // iCloud sync switch; the Engine tab's SyncTileView mirrors the same value. - Settings { - SettingsView() - } - - // Headless surface (M-MXA-7): estate status + reopen + quit; the - // embedded engine stays alive while only this item remains. - MenuBarExtra( - String(localized: "menubar.title", defaultValue: "MOOTx01"), - systemImage: "brain", - isInserted: $menuBarModeEnabled - ) { - MenuBarView(model: model) - } - #endif - } -} - -#if os(iOS) -/// Opportunistic iOS refresh. Cadence remains a request to the system, not a -/// promise of exact execution time. Disabled and unauthorized miners are -/// skipped without prompting. -private enum IOSMiningBackgroundTasks { - static let identifier = "com.codedaptive.mootx01.mining.refresh" - - static func register() { - BGTaskScheduler.shared.register(forTaskWithIdentifier: identifier, using: nil) { task in - guard let refresh = task as? BGAppRefreshTask else { - task.setTaskCompleted(success: false) - return - } - let handle = BackgroundRefreshHandle(refresh) - let work = Task { - do { - let caller = try await GatewayRuntime.shared.bridge() - _ = await MinerRunLoop.liveLoop().tick(now: Date(), caller: caller) - // A4b: the refresh window also drains any spooled shares, - // re-projects the widget snapshot, and runs a sync pass. - await ShareInboxDrain.drainNow() - await WidgetSnapshotRefresher.refreshNow() - await MootSyncDriver.shared.syncNow() - handle.task.setTaskCompleted(success: !Task.isCancelled) - } catch { - handle.task.setTaskCompleted(success: false) - } - await schedule() - } - refresh.expirationHandler = { - work.cancel() - } - } - } - - static func schedule() async { - let request = BGAppRefreshTaskRequest(identifier: identifier) - request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) - try? await BGTaskScheduler.shared.submitTaskRequest(request) - } -} - -private final class BackgroundRefreshHandle: @unchecked Sendable { - let task: BGAppRefreshTask - init(_ task: BGAppRefreshTask) { self.task = task } -} -#endif - -#if os(macOS) -/// A bundled macOS app activates normally; this only forces foreground focus -/// when launched from a tool/`open` so the window comes forward. -final class MacAppDelegate: NSObject, NSApplicationDelegate { - /// M-ING-2 executor: process-lifetime scheduler tick (hourly), alive in - /// headless menu-bar mode where scene tasks are not. Every tick is a - /// no-op until the user enables a source in the Miners tab, and cadence - /// gating (MinerScheduler) decides when an enabled source actually runs. - private var minerTask: Task? - - func applicationDidFinishLaunching(_ notification: Notification) { - NSApplication.shared.setActivationPolicy(.regular) - NSApplication.shared.activate(ignoringOtherApps: true) - minerTask = Task { - let loop = MinerRunLoop.liveLoop() - while !Task.isCancelled { - if let bridge = try? await GatewayRuntime.shared.bridge() { - _ = await loop.tick(now: Date(), caller: bridge) - } - // A4b: headless menu-bar mode still drains spooled shares, - // keeps the widget projection fresh, and runs a sync pass. - await ShareInboxDrain.drainNow() - await WidgetSnapshotRefresher.refreshNow() - await MootSyncDriver.shared.syncNow() - try? await Task.sleep(for: .seconds(3_600)) - } - } - - // APNs push accelerator (CVK-ICLOUD P5-M2): register for remote - // notifications so CloudKit zone-subscription silent pushes arrive. - // Graceful: if entitlement is absent or the user denies (macOS shows no - // prompt for data-delivery-only pushes), registration silently fails and - // polling continues. The resident launchd process cannot hold APNs - // entitlements; registration MUST happen here, in the host app. - // See ConvergenceKit/ZoneSubscription.swift HOST APP CONTRACT, item 2. - NSApplication.shared.registerForRemoteNotifications() - } - - // APNs push accelerator (CVK-ICLOUD P5-M2): forward zone-change silent - // pushes to MootSyncDriver, which delegates to CloudKitSyncEngine. - // CloudKitSyncEngine.handleRemoteNotification(userInfo:) verifies the zone - // name, emits SyncEvent.remoteWakeReceived, and calls nudge() to fire an - // immediate pull and reset the poll tier to fast. - func application(_ application: NSApplication, - didReceiveRemoteNotification userInfo: [String: Any]) { - Task { await MootSyncDriver.shared.handleRemoteNotification(userInfo: userInfo) } - } - - /// M-MXA-7 termination policy: with menu-bar mode ON the app survives - /// its last window closing (headless mining executor, ruling D9); with - /// it OFF the pre-M-MXA-7 quit-on-close behavior is preserved. - func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - MenuBarPolicy.shouldTerminateAfterLastWindowClosed( - menuBarModeEnabled: MenuBarPolicy.isEnabled() - ) - } -} -#endif - -#if os(iOS) -// APNs push accelerator (CVK-ICLOUD P5-M2): UIApplicationDelegate adaptor to -// handle CloudKit silent-push notifications on iOS/iPadOS. -// -// WHY A SEPARATE DELEGATE CLASS: -// SwiftUI's App protocol exposes scene-phase change callbacks but not -// UIApplicationDelegate's push-specific callbacks. An `@UIApplicationDelegateAdaptor` -// bridges the gap without abandoning SwiftUI's lifecycle. The delegate class is -// app-private (no public API surface); it handles only APNs registration results -// and notification forwarding. -// -// UIBackgroundModes remote-notification must be declared in project.yml (done) so -// the OS wakes the app for silent pushes even when in the background. -final class IOSAppDelegate: NSObject, UIApplicationDelegate { - - // Register for APNs at launch so zone-subscription silent pushes start - // arriving as soon as possible. Graceful: Simulator returns an error via - // didFailToRegisterForRemoteNotificationsWithError; production devices without - // a provisioned iCloud container also fail. Polling continues in both cases. - // See ZoneSubscription.swift HOST APP CONTRACT, item 2. - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - application.registerForRemoteNotifications() - return true - } - - // APNs registration result (advisory log only — failure here means - // polling continues without push acceleration; user sees nothing). - func application(_ application: UIApplication, - didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - // CloudKit manages token delivery to its servers via CKContainer - // internally; the host app does not need to forward the token anywhere. - // Logging the hex is useful for debugging silent-push delivery issues. - let hex = deviceToken.map { String(format: "%02x", $0) }.joined() - _ = hex // suppress unused-result; token logged via OS Instruments if needed - } - - func application(_ application: UIApplication, - didFailToRegisterForRemoteNotificationsWithError error: Error) { - // Failure is expected in Simulator (no APNs) and on devices without an - // iCloud-registered bundle ID. Polling continues unchanged. - // No user-visible error — push acceleration is best-effort (B-11). - } - - // Forward zone-change silent pushes to MootSyncDriver (P5-M2). - // completionHandler receives .newData if the engine consumed the push - // (zone matched → nudge fired); .noData otherwise (wrong zone, not a - // CloudKit push, engine not yet enabled). - func application(_ application: UIApplication, - didReceiveRemoteNotification userInfo: [AnyHashable: Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - Task { - let consumed = await MootSyncDriver.shared.handleRemoteNotification(userInfo: userInfo) - completionHandler(consumed ? .newData : .noData) - } - } -} -#endif diff --git a/apps/Mootx01-App/App/Mootx01Shortcuts.swift b/apps/Mootx01-App/App/Mootx01Shortcuts.swift deleted file mode 100644 index eda02e03c..000000000 --- a/apps/Mootx01-App/App/Mootx01Shortcuts.swift +++ /dev/null @@ -1,37 +0,0 @@ -import AppIntents -import MootGateway -import MootIntentKit - -// MARK: - Mootx01Shortcuts (the app-target AppShortcutsProvider) -// -// The AppShortcutsProvider MUST live in the app target (not the linked -// package) for the App Intents metadata extractor to register these with the -// system — which is what makes them appear in the Shortcuts app and Siri, and -// callable for real. The intent TYPES live in MootIntentKit (shared); the app -// declares which ones it publishes and their invocation phrases. -// -// Only the two phrase-friendly verbs are auto-donated; the rest stay -// Shortcuts-composable as plain App Intents. - -struct Mootx01Shortcuts: AppShortcutsProvider { - static var appShortcuts: [AppShortcut] { - AppShortcut( - intent: CaptureDrawerIntent(), - phrases: [ - "Capture this in \(.applicationName)", - "Remember this with \(.applicationName)", - ], - shortTitle: "Capture Memory", - systemImageName: "tray.and.arrow.down" - ) - AppShortcut( - intent: RecallDrawerIntent(), - phrases: [ - "Recall from \(.applicationName)", - "Search my memories in \(.applicationName)", - ], - shortTitle: "Recall Memories", - systemImageName: "tray.and.arrow.up" - ) - } -} diff --git a/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/Contents.json index 9a1e1aafa..7a0422b03 100644 --- a/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -5,6 +5,66 @@ "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" + }, + { + "filename" : "icon_mac_16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "icon_mac_32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "icon_mac_32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "icon_mac_64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "icon_mac_128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "icon_mac_256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "icon_mac_256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "icon_mac_512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "icon_mac_512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "icon_1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } diff --git a/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_128.png b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_128.png new file mode 100644 index 000000000..517d2d0ad Binary files /dev/null and b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_128.png differ diff --git a/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_16.png b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_16.png new file mode 100644 index 000000000..42678ff1a Binary files /dev/null and b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_16.png differ diff --git a/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_256.png b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_256.png new file mode 100644 index 000000000..9e170b73e Binary files /dev/null and b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_256.png differ diff --git a/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_32.png b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_32.png new file mode 100644 index 000000000..2bbf0031c Binary files /dev/null and b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_32.png differ diff --git a/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_512.png b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_512.png new file mode 100644 index 000000000..a78e7df03 Binary files /dev/null and b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_512.png differ diff --git a/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_64.png b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_64.png new file mode 100644 index 000000000..fbd736855 Binary files /dev/null and b/apps/Mootx01-App/Assets.xcassets/AppIcon.appiconset/icon_mac_64.png differ diff --git a/apps/Mootx01-App/CommunityApp/Mootx01-Community-macOS.entitlements b/apps/Mootx01-App/CommunityApp/Mootx01-Community-macOS.entitlements new file mode 100644 index 000000000..703c92afc --- /dev/null +++ b/apps/Mootx01-App/CommunityApp/Mootx01-Community-macOS.entitlements @@ -0,0 +1,20 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.files.user-selected.read-write + + com.apple.security.application-groups + + G94X5T5GK7.group.com.codedaptive.mootx01 + + keychain-access-groups + + $(AppIdentifierPrefix)com.codedaptive.mootx01.shared + + + diff --git a/apps/Mootx01-App/CommunityApp/Mootx01CommunityApp.swift b/apps/Mootx01-App/CommunityApp/Mootx01CommunityApp.swift new file mode 100644 index 000000000..cb54ffd92 --- /dev/null +++ b/apps/Mootx01-App/CommunityApp/Mootx01CommunityApp.swift @@ -0,0 +1,67 @@ +import AppKit +import MootCommunityUI +import SwiftUI + +@main +struct Mootx01CommunityApp: App { + @NSApplicationDelegateAdaptor(CommunityAppDelegate.self) private var delegate + @State private var model = CommunityAppModel() + + var body: some Scene { + WindowGroup { + CommunityContentView(model: model) + .frame(minWidth: 760, minHeight: 520) + .task { await model.maintainConnection() } + } + .commands { CommunityCaptureCommands(model: model) } + + Window(String(localized: "Quick Capture"), id: "quick-capture") { + Group { + if model.isEstateReady { + CommunityCaptureView(model: model.captureModel, compact: true) + } else { + ContentUnavailableView( + String(localized: "Quick Capture unavailable"), + systemImage: "externaldrive.badge.exclamationmark", + description: Text(model.status) + ) + } + } + .frame(minWidth: 420, minHeight: 420) + } + } +} + +private struct CommunityCaptureCommands: Commands { + @Environment(\.openWindow) private var openWindow + let model: CommunityAppModel + + var body: some Commands { + CommandGroup(after: .newItem) { + Button(String(localized: "Quick Capture")) { + openWindow(id: "quick-capture") + } + .keyboardShortcut("n", modifiers: [.command, .shift]) + .disabled(!model.isEstateReady) + } + } +} + +final class CommunityAppDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + NSApplication.shared.setActivationPolicy(.regular) + NSApplication.shared.activate(ignoringOtherApps: true) + } + + func applicationShouldHandleReopen( + _ sender: NSApplication, + hasVisibleWindows flag: Bool + ) -> Bool { + if !flag { + sender.windows.first?.makeKeyAndOrderFront(nil) + } + return true + } + + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true } +} diff --git a/apps/Mootx01-App/CommunityApp/en.lproj/Localizable.strings b/apps/Mootx01-App/CommunityApp/en.lproj/Localizable.strings new file mode 100644 index 000000000..97c4282bb --- /dev/null +++ b/apps/Mootx01-App/CommunityApp/en.lproj/Localizable.strings @@ -0,0 +1,182 @@ +/* Community 1.1 feature UI — English source localization. */ + +"review.kind.morning" = "Morning Review"; +"review.kind.end.of.day" = "End of Day Review"; +"review.kind.weekly" = "Weekly Review"; +"review.completion.failed.title" = "Review Completion Failed"; +"review.completion.failed.a11y %@" = "Review completion failed: %@"; + +"obsidian.section.authorization" = "Vault Authorization"; +"obsidian.section.status" = "Synchronization Status"; +"obsidian.action.select.vault" = "Select Vault…"; +"obsidian.action.select.vault.a11y" = "Select or replace the Obsidian vault"; +"obsidian.action.enable" = "Enable Synchronization"; +"obsidian.action.enable.a11y" = "Enable continuous Obsidian synchronization"; +"obsidian.action.disable" = "Disable Synchronization"; +"obsidian.action.disable.a11y" = "Disable continuous Obsidian synchronization"; +"obsidian.action.retry" = "Retry Synchronization"; +"obsidian.action.retry.a11y" = "Retry the interrupted Obsidian synchronization"; +"obsidian.action.retry.hint" = "Available only when the daemon says the condition is retryable."; +"obsidian.auth.missing" = "No vault is authorized."; +"obsidian.auth.needs.renewal %@" = "Authorization for %@ needs renewal."; +"obsidian.auth.valid %@" = "%@ is authorized."; +"obsidian.auth.valid.a11y %@" = "Obsidian vault %@ is authorized."; +"obsidian.outcome.vault.denied %@" = "Vault selection denied: %@"; +"obsidian.outcome.vault.denied.a11y %@" = "Obsidian vault selection denied: %@"; +"obsidian.outcome.enable.refused %@" = "Enable synchronization refused: %@"; +"obsidian.outcome.enable.refused.a11y %@" = "Obsidian synchronization enable request refused: %@"; +"obsidian.outcome.enable.failed %@" = "Enable synchronization failed: %@"; +"obsidian.outcome.retry.refused %@" = "Retry refused: %@"; +"obsidian.outcome.retry.refused.a11y %@" = "Obsidian synchronization retry refused: %@"; +"obsidian.outcome.retry.failed %@" = "Retry failed: %@"; +"obsidian.report.disabled.and.removed" = "Synchronization is disabled and the daemon reports that synchronized data was removed."; +"obsidian.report.disabled.only" = "Synchronization is disabled. No data removal was reported."; +"obsidian.status.blocked" = "Synchronization Blocked"; +"obsidian.status.blocked.a11y" = "Obsidian synchronization is blocked"; +"obsidian.status.checkpoint %lld" = "Last checkpoint synchronized %lld records."; +"obsidian.status.failed" = "Synchronization Failed"; +"obsidian.status.idle" = "Up to Date"; +"obsidian.status.interrupted" = "Synchronization Interrupted"; +"obsidian.status.loading.a11y" = "Loading Obsidian synchronization status"; +"obsidian.status.paused" = "Synchronization Paused"; +"obsidian.status.progress %lld %lld" = "%lld of %lld records remain."; +"obsidian.status.scanning" = "Scanning Vault…"; +"obsidian.status.starting" = "Starting Synchronization…"; +"obsidian.status.synchronizing" = "Synchronizing…"; +"obsidian.status.waiting" = "Waiting to Synchronize"; + +"lan.section.policy" = "Sharing Policy"; +"lan.section.status" = "Serving Status"; +"lan.action.refresh.eligibility" = "Refresh Eligibility"; +"lan.action.refresh.eligibility.a11y" = "Refresh LAN sharing eligibility"; +"lan.action.start" = "Start LAN Sharing"; +"lan.action.start.a11y" = "Start portable LAN sharing"; +"lan.action.start.hint" = "The daemon verifies authorization and privacy eligibility before serving."; +"lan.action.stop" = "Stop LAN Sharing"; +"lan.action.stop.a11y" = "Stop portable LAN sharing"; +"lan.action.stop.hint" = "The status changes to stopped only after daemon confirmation."; +"lan.auth.expired" = "Authentication Expired"; +"lan.auth.not.obtained" = "Authentication Not Obtained"; +"lan.auth.valid" = "Authentication Valid"; +"lan.outcome.denied %@" = "Start denied: %@"; +"lan.outcome.failed %@" = "Start failed: %@"; +"lan.outcome.stop.failed %@" = "Stop failed: %@"; +"lan.outcome.eligibility.refused %@" = "Eligibility refresh refused: %@"; +"lan.outcome.eligibility.refused.a11y %@" = "LAN sharing eligibility refresh refused: %@"; +"lan.outcome.eligibility.failed %@" = "Eligibility refresh failed: %@"; +"lan.policy.eligible %lld" = "Eligible records: %lld"; +"lan.policy.eligible.a11y %lld" = "%lld records are eligible for LAN sharing"; +"lan.policy.excluded %lld" = "Excluded records: %lld"; +"lan.policy.excluded.a11y %lld" = "%lld records are excluded from LAN sharing"; +"lan.policy.loading.a11y" = "Loading LAN sharing policy"; +"lan.policy.blocked %@" = "Policy unavailable; showing last confirmed values: %@"; +"lan.policy.failed %@" = "Policy refresh failed; values are not confirmed: %@"; +"lan.status.active" = "LAN Sharing Active"; +"lan.status.blocked" = "LAN Sharing Blocked"; +"lan.status.blocked.a11y" = "LAN sharing is blocked"; +"lan.status.endpoint.a11y" = "LAN sharing endpoint"; +"lan.status.failed" = "LAN Sharing Failed"; +"lan.status.interrupted" = "LAN Sharing Interrupted"; +"lan.status.loading.a11y" = "Loading LAN sharing status"; +"lan.status.starting" = "Starting LAN Sharing…"; +"lan.status.stopped" = "LAN Sharing Off"; + +"transfer.root.label" = "Community Import and Export"; +"transfer.mode.picker.label" = "Transfer Mode"; +"transfer.mode.picker.accessibility" = "Choose import or export"; +"transfer.mode.import.label" = "Import"; +"transfer.mode.export.label" = "Export"; +"transfer.import.source.heading" = "Import Source"; +"transfer.import.source.none" = "No import source selected."; +"transfer.import.source.none.value" = "Select a source before planning an import."; +"transfer.import.source.cancelled" = "Source selection cancelled."; +"transfer.import.source.denied" = "The import source was denied."; +"transfer.import.select.source.button" = "Select Source…"; +"transfer.import.select.source.accessibility" = "Select an import source"; +"transfer.import.plan.heading" = "Import Plan"; +"transfer.import.plan.button" = "Plan Import"; +"transfer.import.plan.button.accessibility" = "Ask the daemon to plan the import"; +"transfer.import.plan.button.hint.no.source" = "Select an import source first."; +"transfer.import.plan.refresh.button" = "Choose Again and Replan"; +"transfer.import.plan.refresh.accessibility" = "Choose another source and create a new import plan"; +"transfer.import.execute.button" = "Run Import"; +"transfer.import.execute.button.accessibility" = "Execute the daemon-approved import plan"; +"transfer.import.execute.button.hint.no.plan" = "A permitted daemon plan is required before import."; +"transfer.import.job.heading" = "Import Job"; +"transfer.import.job.refresh.button" = "Refresh Status"; +"transfer.import.job.refresh.accessibility" = "Refresh import job status"; +"transfer.import.cancel.button" = "Cancel Import"; +"transfer.import.cancel.button.accessibility" = "Request cancellation of the import job"; +"transfer.export.destination.heading" = "Export Destination"; +"transfer.export.destination.none" = "No export destination selected."; +"transfer.export.destination.cancelled" = "Destination selection cancelled."; +"transfer.export.destination.denied" = "The export destination was denied."; +"transfer.export.select.destination.button" = "Select Destination…"; +"transfer.export.select.destination.accessibility" = "Select an export destination"; +"transfer.export.scope.heading" = "Export Scope"; +"transfer.export.scope.none" = "No export scope selected."; +"transfer.export.scope.cancelled" = "Scope selection cancelled."; +"transfer.export.scope.count %lld" = "%lld candidate records"; +"transfer.export.select.scope.button" = "Select Scope…"; +"transfer.export.select.scope.accessibility" = "Select a daemon-approved export scope"; +"transfer.export.plan.heading" = "Export Plan"; +"transfer.export.plan.button" = "Plan Export"; +"transfer.export.plan.button.accessibility" = "Ask the daemon to plan the export"; +"transfer.export.execute.button" = "Run Export"; +"transfer.export.execute.button.accessibility" = "Execute the daemon-approved export plan"; +"transfer.export.execute.button.hint.no.plan" = "A permitted daemon plan is required before export."; +"transfer.export.job.heading" = "Export Job"; +"transfer.export.job.refresh.button" = "Refresh Status"; +"transfer.export.job.refresh.accessibility" = "Refresh export job status"; +"transfer.export.cancel.button" = "Cancel Export"; +"transfer.export.cancel.button.accessibility" = "Request cancellation of the export job"; +"transfer.outcome.import.plan.failed %@" = "Import planning failed: %@"; +"transfer.outcome.import.execute.denied %@" = "Import denied: %@"; +"transfer.outcome.import.execute.denied.a11y %@" = "Import execution denied: %@"; +"transfer.outcome.import.execute.failed %@" = "Import failed: %@"; +"transfer.outcome.cancel.not.found" = "The transfer job was not found."; +"transfer.outcome.cancel.failed %@" = "Cancellation failed: %@"; +"transfer.outcome.job.status.not.found" = "The daemon no longer recognizes this transfer job. The displayed state is the last confirmed state."; +"transfer.outcome.job.status.failed %@" = "Transfer status could not be confirmed: %@"; +"transfer.outcome.export.plan.failed %@" = "Export planning failed: %@"; +"transfer.outcome.export.execute.denied %@" = "Export denied: %@"; +"transfer.outcome.export.execute.denied.a11y %@" = "Export execution denied: %@"; +"transfer.outcome.export.execute.failed %@" = "Export failed: %@"; +"transfer.format.recognized %@" = "Recognized format: %@"; +"transfer.format.recognized.accessibility %@" = "Recognized transfer format %@"; +"transfer.format.unrecognized %@" = "Unrecognized format: %@"; +"transfer.format.unrecognized.accessibility %@" = "Unrecognized transfer format %@"; +"transfer.plan.format.label" = "Format"; +"transfer.plan.format.unrecognized.badge" = "Unrecognized"; +"transfer.plan.format.unrecognized.accessibility" = "The daemon does not recognize this format"; +"transfer.plan.candidates.label" = "Candidates"; +"transfer.plan.conflicts.label" = "Conflicts"; +"transfer.plan.invalid.label" = "Invalid"; +"transfer.plan.excluded.label" = "Excluded by Policy"; +"transfer.plan.estimated.import.label" = "Will Import"; +"transfer.plan.estimated.export.label" = "Will Export"; +"transfer.plan.execution.refused" = "Execution Not Permitted"; +"transfer.plan.execution.refused.accessibility" = "The daemon did not permit execution of this plan"; +"transfer.job.state.loading" = "Loading job status…"; +"transfer.job.state.queued" = "Queued"; +"transfer.job.state.running" = "Running"; +"transfer.job.state.waiting" = "Waiting"; +"transfer.job.state.failed" = "Failed"; +"transfer.job.state.failed.partial" = "Some work was committed before failure."; +"transfer.job.progress %lld %lld" = "%lld of %lld records processed"; +"transfer.job.progress.accessibility %lld %lld" = "%lld of %lld records processed"; +"transfer.cancel.stage.before.commit" = "Cancelled Before Commit"; +"transfer.cancel.stage.before.commit.accessibility" = "Cancelled before any work was committed"; +"transfer.cancel.stage.during.commit" = "Cancelled During Commit"; +"transfer.cancel.stage.during.commit.accessibility" = "Cancelled while work was being committed"; +"transfer.cancel.stage.after.commit" = "Cancelled After Commit"; +"transfer.cancel.stage.after.commit.accessibility" = "Cancelled after work was committed"; +"transfer.counts.complete.heading" = "Completed"; +"transfer.counts.partial.heading" = "Partial Results"; +"transfer.counts.transferred.label" = "Transferred"; +"transfer.counts.skipped.label" = "Skipped"; +"transfer.counts.conflicted.label" = "Conflicted"; +"transfer.counts.excluded.label" = "Excluded"; +"transfer.counts.failed.label" = "Failed"; +"transfer.counts.receipt.label" = "Receipt"; +"transfer.counts.receipt.accessibility %@" = "Transfer receipt %@"; diff --git a/apps/Mootx01-App/CommunityUITestHost/CommunityUITestHostApp.swift b/apps/Mootx01-App/CommunityUITestHost/CommunityUITestHostApp.swift new file mode 100644 index 000000000..373ae91c1 --- /dev/null +++ b/apps/Mootx01-App/CommunityUITestHost/CommunityUITestHostApp.swift @@ -0,0 +1,16 @@ +import MootCommunityUI +import MootCommunityUITestSupport +import SwiftUI + +@main +struct CommunityUITestHostApp: App { + @State private var model = CommunityUITestModelFactory.makeReadyModel() + + var body: some Scene { + WindowGroup { + CommunityContentView(model: model) + .frame(minWidth: 900, minHeight: 680) + .task { await model.start() } + } + } +} diff --git a/apps/Mootx01-App/CommunityUITests/CommunitySurfaceUITests.swift b/apps/Mootx01-App/CommunityUITests/CommunitySurfaceUITests.swift new file mode 100644 index 000000000..d60d94e8c --- /dev/null +++ b/apps/Mootx01-App/CommunityUITests/CommunitySurfaceUITests.swift @@ -0,0 +1,60 @@ +import XCTest + +@MainActor +final class CommunitySurfaceUITests: XCTestCase { + func testCapturePrivacyControlsAreReachableAndIndependent() { + continueAfterFailure = false + let app = XCUIApplication() + app.launch() + defer { app.terminate() } + + let destination = element("community.capture.destination", in: app) + let sensitivity = element("community.capture.sensitivity", in: app) + let exportEligibility = element("community.capture.export-eligibility", in: app) + let lanEligibility = element("community.capture.lan-eligibility", in: app) + + XCTAssertTrue(destination.waitForExistence(timeout: 10)) + XCTAssertTrue(sensitivity.exists) + XCTAssertTrue(exportEligibility.exists) + XCTAssertTrue(lanEligibility.exists) + XCTAssertEqual(exportEligibility.label, "Eligible for export") + XCTAssertEqual(lanEligibility.label, "Eligible for LAN sharing") + XCTAssertNotEqual(exportEligibility.identifier, lanEligibility.identifier) + } + + func testEveryCommunityOperationsSurfaceIsReachableByStableIdentifier() { + continueAfterFailure = false + let app = XCUIApplication() + app.launch() + defer { app.terminate() } + + let operations = element("community.destination.operations", in: app) + XCTAssertTrue(operations.waitForExistence(timeout: 10)) + operations.click() + + XCTAssertTrue( + element("community.operations.workspace", in: app).waitForExistence(timeout: 5) + ) + + let destinations = [ + ("workspace.review", "community.operations.review"), + ("workspace.obsidian", "community.operations.obsidian"), + ("workspace.transfer", "community.operations.transfer"), + ("workspace.lan", "community.operations.lan"), + ] + for (identifier, surfaceIdentifier) in destinations { + let row = element(identifier, in: app) + XCTAssertTrue(row.exists, "Missing operations row \(identifier)") + XCTAssertFalse(row.label.isEmpty, "Operations row \(identifier) has no accessible label") + row.click() + XCTAssertTrue( + element(surfaceIdentifier, in: app).waitForExistence(timeout: 3), + "Operations row \(identifier) did not expose \(surfaceIdentifier)" + ) + } + } + + private func element(_ identifier: String, in app: XCUIApplication) -> XCUIElement { + app.descendants(matching: .any).matching(identifier: identifier).firstMatch + } +} diff --git a/apps/Mootx01-App/LEXICON_TO_APPLE_MAPPING.md b/apps/Mootx01-App/LEXICON_TO_APPLE_MAPPING.md deleted file mode 100644 index d8cfd28c5..000000000 --- a/apps/Mootx01-App/LEXICON_TO_APPLE_MAPPING.md +++ /dev/null @@ -1,185 +0,0 @@ -# ARIA Lexicon → Apple Surfaces — Mapping - -**Status:** built with the GatewaySpike (2026-06-07), grounded against the substrate -codebase. The lexicon side is fixed (AriaLexiconLib: one noun, nine verbs, four adjectives, -invariants I-7/I-8). The Apple adapter implementations live in two places: substrate-facing -seam code in `Sources/MootGateway/` (MootBridge, GatewayTransport, MootEstateClient, -AdapterStatus, LexiconMap) and the Apple-framework-dependent intents in -`packages/apple/MootIntentKit/Sources/MootIntentKit/` (verb intents, MootURLRouter, -MootShortcutsProvider). WWDC landed 2026-06-08; §5 tracks what changed. When a surface -changes, the delta lands in one of three mirrored places — this table, the matching adapter -file, and `Sources/MootGateway/LexiconMap.swift` — and §5 below says exactly which. - -This document is the prose mirror of `LexiconMap.swift`; that file is the executable source of -truth. If they drift, the code wins and this doc is wrong. - ---- - -## 0. Grounded facts (what the prototype proved) - -- ARIA is reached **in-process** (A1): `GeniusLocusKit` opened directly, driven through the - ARIA_MCP dispatcher with **no transport** (`MootBridge`). Every adapter talks to the substrate - the way a remote MCP client will — just without a wire. -- The tool surface is the **53 `moot_*` interface** (decision MCP-INT-01), *not* the - `drawer_recall`/`capture_drawer` lexicon-projected names. The 53 tools span five tiers - (Tier 1–5: 19 core tools) plus Federation (1), Recipe (7), Lens (21), and Vault (5). - Internally `moot_file_memory → kit.capture`, `moot_memory_search → kit.recall`, etc. - Source of truth: `packages/kits/AriaMcpKit/Sources/AriaMCP/ToolProjection.swift` (Tiers 1–5 + Federation) - plus `LensTools.swift`, `RecipeTools.swift`, `VaultTools.swift`; Rust mirror in - `packages/kits/AriaMcpKit/rust/src/tool_list.rs` (header comment confirms 53). -- **ARIA is always the server** (ARIA_MCP_SPEC §5). The consume-other-estate leg (A3) is a - *separate client component* (`MootEstateClient`), never ARIA_MCP acting as a client. -- The native adapters (App Intents/Shortcuts/callback-URL) are **implemented and tested** in - `packages/apple/MootIntentKit/`. They are not yet *system-registered* — that requires an Xcode - app bundle (`AppIntentsPackage` declaration + `CFBundleURLTypes` in Info.plist). The - Share-Sheet `NSExtension` target is not yet built (see §6). System registration is the - remaining graduation step; capability is not the gap. - ---- - -## 1. Master verb table - -Noun is always **Drawer**. Flow: who may invoke (caller-driven / Brain-emitted / grounding). - -| Verb | Flow | Dir | `moot_*` tool | App Intent shell | Apple reach | x-callback | Caller? | -|---|---|---|---|---|---|---|---| -| **capture** | caller | WRITE | `moot_file_memory` | `CaptureDrawerIntent` | Share Sheet · Shortcuts · Siri · Action Button | — rejected (mutates the estate; App Intents only) | ✅ | -| **recall** | caller | READ | `moot_memory_search` | `RecallDrawerIntent` | Siri · Spotlight · Shortcuts · Action Button | `…/recall?query=&filter=` | ✅ | -| **reanchor** | caller | STRUCT | `moot_move_memory` | `ReanchorDrawerIntent` | Shortcuts | — rejected (mutates the estate; App Intents only) | ✅ | -| **mutate** | caller | WRITE | `moot_update_memory` | `MutateDrawerIntent` | Shortcuts | `…/mutate?id=&mutation=` | ✅ | -| **withdraw** | caller | WRITE | `moot_withdraw_memory` | `WithdrawDrawerIntent` | Shortcuts | `…/withdraw?id=` | ✅ | -| **expunge** | caller | WRITE | `moot_erase_memory` | `ExpungeDrawerIntent` | Shortcuts (guarded) | `…/expunge?id=&reason=&confirmed=` | ✅ | -| **propose** | **Brain** | — | — | — (elicitation, later) | — | — | ❌ | -| **associate** | **Brain** | — | — | — | — | — | ❌ | -| **learn** | grounding | WRITE | — | — (fed by A3) | — | — | ❌ | - -**Submit-in (A4b)** = `capture`. It is a caller-driven *core* verb — no dreaming/propose gate — -so it works the moment a bundle registers it. **Serve-out (A4a/A5)** = `recall`, filtered by the -export policy (§3). The two Brain-emitted verbs are not tools and not Apple-invokable; their -natural Apple home is **App Intents elicitation** (confirm a proposal), a post-WWDC mapping. - -### Assistant-schema candidacy - -No Apple **assistant schema** has a "memory"/"knowledge" domain today, so every ARIA op is a -**custom** App Intent. If WWDC introduces a memory/notes/knowledge schema, `recall` (a retrieval -intent) and `capture` (a create intent) are the first candidates to conform — see §5. - ---- - -## 2. Noun mapping — Drawer → `DrawerEntity` - -`Drawer` (LocusKit) → `DrawerEntity` (`AppEntity`), so Siri/Spotlight/Shortcuts can carry a -memory between steps and index it. - -| Drawer field | DrawerEntity | Notes | -|---|---|---| -| `id` | `id` (`AppEntity.ID`) | Stable across recall / Spotlight / chaining — the contract that lets a Shortcut recall then act. | -| `content` | `content` (`@Property`) + `DisplayRepresentation.title` | Verbatim; immutable at the core. | -| `room` | `room` (`@Property`) + subtitle | Structural coordinate; recall can group by it. | -| `adjectiveBitmap` → state/trust/sensitivity/exportability | read-only context | Set by capture; surfaced, not user-edited on the entity. | - -**DrawerEntity recall is wired via structured recall results.** `moot_memory_search` -replies carry a `structuredContent` block of typed `{id, room, content, subject}` rows -(declared by the tool's `outputSchema`). `StructuredRecallResults` (in MootIntentKit) decodes -typed `DrawerEntity` values from that block at the gateway layer — entity data never comes -from the display text, whose interpolated drawer content is caller-controlled. - -- `DrawerEntityQuery.entities(for:)` resolves by running a recall with the UUID as the query - and exact-id filtering; best-effort but no fabrication. -- `DrawerEntityQuery.suggestedEntities()` returns the 20 most-recent drawers. -- `RecallDrawerIntent` returns a typed `[DrawerEntity]` value plus the full response text as - dialog — Shortcuts chains the entities into a next step; Siri reads the dialog. One - `moot_memory_search` call feeds both (composition: `RecallDrawerIntent.entities(from:)`). - -Content in `DrawerEntity` is the drawer body from the structured recall row; restricted and -secret drawers carry the server's redaction marker instead of the body. - ---- - -## 3. Adjective mapping (invariant I-8) - -| Adjective | Values | Apple role | -|---|---|---| -| `state` | active · pending · contested · superseded · decayed · withdrawn · expired · rejected · accepted · tombstoned | Recall context; not a capture parameter. | -| `trust` | verbatim · observed · imported · proposed · derived · canonical | Set by capture channel; read-only on the entity. | -| `sensitivity` | normal · elevated · restricted · secret | **Capture parameter** (`SensitivityAppEnum`) + recall ceiling. Raw values match the tool's `decodeSensitivity` exactly — straight through, no mapping. | -| `exportability` | private · public | **Serve-out gate (§6.2):** recall `filter:exportable` exposes only public rows. | - -**Edge — export policy write side is now live.** The bitmap has `exportability` (private=0/public=32) -and both the write path and read path are wired: - -- **At capture:** `moot_file_memory` accepts an optional `exportability` argument (`"private"` | - `"public"`). Default is private. Drawers born public are immediately returned by - `filter:exportable` recall. Wired in `ToolDispatch.decodeExportability` and applied to the - `CaptureFrame` before the write. -- **Post-capture promotion:** `moot_update_memory` accepts `mutation=correctExportability(public)` - or `mutation=correctExportability(private)`. Decoded by `ToolDispatch.decodeMutationKind` as - `MutationKind.correctExportability(.public_)` / `.correctExportability(.private_)`. - -**All three write surfaces are now live:** - -- **CaptureView:** exposes a private / public Picker that passes `exportability` to - `moot_file_memory` at capture time. The status in `AdapterStatus.findings` reflects this. -- **Tool path:** `moot_file_memory` with `exportability:"public"` (unchanged). -- **Post-capture promotion:** `moot_update_memory correctExportability(public)` (unchanged). - -A4a serve-out via `filter:exportable` returns correctly populated results from all three -write paths. - ---- - -## 4. Adapter inventory (A1–A6) and where each lives - -| Code | Adapter | State | File | -|---|---|---|---| -| A1 | Embedded (in-process) | **live** | `Sources/MootGateway/MootBridge.swift` | -| A2 | ARIA_MCP server on device | **live (app-hosted) / seam (daemon)** | `Sources/MootGateway/Transport/GatewayTransport.swift` (`HTTPTransport` loopback client + `InProcessTransport`) and `Sources/MootGateway/LANServer/` — `MootLANServer` (NWListener) serves the app's OWN estate to LAN MCP clients over credentialed HTTP/JSON-RPC, bridging to the in-process dispatcher; it advertises `_mootx01._tcp` and `LANDaemonBrowser` discovers peers. Remote callers are bearer-authed, read-only, and public-only (`LANRequestGate`). The STANDALONE-daemon leg stays seam: the daemon advertising itself is an engine-lane parity mission. | -| A3 | Consume other estates (client) | **v1.1** | `Sources/MootGateway/MCPClient/MootEstateClient.swift` — fold-in via capture is real (`foldIn`); outbound federation deferred to v1.1 by Bob's ruling; `fetch` throws `outboundFederationNotInThisVersion` as an explicit guard. | -| A4 | App Intents | **pending registration** | `packages/apple/MootIntentKit/Sources/MootIntentKit/CaptureDrawerIntent.swift` + `RecallDrawerIntent.swift` + other verb intents — implementation is live and tested; `Mootx01Shortcuts.updateAppShortcutParameters()` called at every app launch (App/Mootx01App.swift). System Siri/Spotlight activation requires the Xcode app bundle build (xcodegen → xcodebuild). | -| A5 | Callback URL | **pending registration** | `packages/apple/MootIntentKit/Sources/MootIntentKit/MootURLRouter.swift` — a READ-ONLY surface: the verb allowlist admits `recall` only; mutating verbs (capture, reanchor) are rejected because an inbound URL carries no trustworthy caller identity — mutations go through the consented App Intents path. Routing and hardening are tested; `CFBundleURLTypes` for `mootx01://` is declared in `project.yml` (the xcodegen spec). URL-scheme registration activates when xcodegen regenerates the project and the app bundle is built. | -| A6 | Shortcuts library | **pending registration** | `packages/apple/MootIntentKit/Sources/MootIntentKit/MootShortcutsProvider.swift` (all six intents) + `App/Mootx01Shortcuts.swift` (the app-target `AppShortcutsProvider`, capture + recall). Both donate via `updateAppShortcutParameters()` at launch. Phrases appear in the Shortcuts app once the xcodegen-derived app bundle is built and installed. | - -The authoritative adapter state is `Sources/MootGateway/AdapterStatus.swift` (`GatewayEdges.adapters`), -which the Edges tab reads at runtime. If this table and AdapterStatus.swift ever diverge, AdapterStatus wins. - -A2's loopback-HTTP transport (`HTTPTransport`) is implemented (URLSession POST, full error taxonomy, -JSON-RPC 2.0 decode). The capability gap is LAN/Bonjour discovery and `NSLocalNetworkUsageDescription` -— not the HTTP wire itself. Seam status reflects that `HTTPTransport` is not yet wired as the default -path. Enterprise OAuth (EE) composes above this transport in v2. - ---- - -## 5. WWDC reaction deltas (the small-finish map) - -Keynote Mon 2026-06-08. For each branch, the *exact* file(s) the delta lands in. This is what -turns "Apple dropped a change" into "complete the slot." - -| If Apple announces… | Land the delta in… | Size | -|---|---|---| -| **Apple Intelligence dials out as an MCP client** | `Sources/MootGateway/Transport/GatewayTransport.swift` — adapt `HTTPTransport` to Apple's transport/auth; the resident daemon supplies the listener. No verb/tool change. | Small | -| **App Intents exported outward as MCP** | Nothing new to author — the A4 intents in `packages/apple/MootIntentKit/` are the on-ramp; register them in an Xcode bundle (`AppIntentsPackage`). | Medium (bundle) | -| **FM v2 / on-device "Core AI" tool-calling** | New `FMToolAdapter` beside MootIntentKit exposing `recall`/`capture` as a model `Tool`; reuse `MootBridge`. | Small | -| **App Intents 2.0 (richer entities / streaming)** | `packages/apple/MootIntentKit/Sources/MootIntentKit/DrawerEntity.swift` — adopt the new entity/result types. The typed `[DrawerEntity]` recall result is wired (RecallDrawerIntent returns entities + dialog); remaining upgrades are streaming results and richer entity properties. | Enhance | -| **A memory/knowledge assistant schema** | `packages/apple/MootIntentKit/Sources/MootIntentKit/CaptureDrawerIntent.swift` + `RecallDrawerIntent.swift` — conform to the schema for the deep treatment; update §1 candidacy. | Opportunistic | -| **Personal Context APIs open** | New scope doc + an `A3` reader in `MCPClient/` consuming Personal Context; fold in via `capture`/`learn`. | Investigate | - -Each row touches one shell file plus, where relevant, this table and `LexiconMap.swift`. The -verb set never changes — the lexicon is fixed; only its Apple projection moves. - ---- - -## 6. Out of scope (made visible, not filled) - -**Closed since last audit (2026-06-13):** CaptureView exportability Picker (A), DrawerEntity -structured recall via gateway-layer text parse (B), `updateAppShortcutParameters()` at launch -and `CFBundleURLTypes` declared in `project.yml` (C), typed `[DrawerEntity]` recall results (D), -and the `NSExtension` Share-Sheet capture targets (E — `Mootx01-Share-iOS`/`-macOS`, UI-less: -the extension spools to the app-group `ShareInbox`, the host app drains via `ShareInboxDrain` -at launch/foreground/tick; the extension process never opens the estate). - -**Remaining:** daemon-side Bonjour advertisement (the last A2 gap — an engine-lane Swift/Rust -parity mission; the app-side browse client `LANDaemonDiscovery` and the `NSBonjourServices` / -`NSLocalNetworkUsageDescription` plumbing are done, and loopback `HTTPTransport` is implemented); -iCloud sync; graduating `MootGateway` into `ARIA_MacOS`/`ARIA_iOS`. System activation of -A4/A5/A6 requires running `xcodegen generate` then building the Xcode project — that Xcode -build step is outside SPM and is the final activation gate. diff --git a/apps/Mootx01-App/Package.resolved b/apps/Mootx01-App/Package.resolved index c1a617a8f..4ce47cef4 100644 --- a/apps/Mootx01-App/Package.resolved +++ b/apps/Mootx01-App/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "742040f68d3215bb9e8f36642c4b0b3da7c19dc645c7b8beff6a6245c63ce013", + "originHash" : "05c4abbda5007ad3dc9d750acde27fe1699c8dfb15a99af772294d1741aa5816", "pins" : [ { "identity" : "postgres-nio", diff --git a/apps/Mootx01-App/Package.swift b/apps/Mootx01-App/Package.swift index d648a1857..da2b04c5b 100644 --- a/apps/Mootx01-App/Package.swift +++ b/apps/Mootx01-App/Package.swift @@ -1,131 +1,44 @@ // swift-tools-version:6.2 -// -// Mootx01 — the Apple presentation layer over the clean MOOTx01 engine. -// -// This SwiftPM package vends the two Swift-only, Apple-side libraries the -// Mootx01 app (and the developer example apps) build on. It is the "Apple -// layer" of the app/engine boundary: it ENVELOPES the clean, Rust-mirrored engine through a -// transport seam — it never absorbs it. The headless `mootx01`/`aria-mcp` -// server is a separate, untouched binary. -// -// - MootGateway — the substrate-facing bridge: MootBridge (drives the ARIA -// tool surface), the transport modes (embedded / managed-subprocess / -// HTTP-seam), the host/handoff logic. MootBridge conforms to -// MootToolCalling (from MootIntentKit) so the intent layer never imports -// the substrate directly. The lexicon→Apple mapping data. -// - GatewayUI — the shared SwiftUI surface (model + views) reused by the -// macOS and iOS app targets (built via the xcodegen project here). -// -// The intent surface (App Intents, Shortcuts, callback-URL, Share Sheet) lives -// in MootIntentKit at packages/apple/MootIntentKit — consumed here by MootGateway. -// -// The runnable app targets live in the Xcode project (project.yml → xcodegen) -// because real, system-registered App Intents require an app bundle. This -// package has no executable target; `swift build`/`swift test` exercise the -// libraries headlessly. -// -// Platforms: macOS 27 / iOS 27 (Apple Silicon) — the app leads the kit stack -// here deliberately (Bob ruling 2026-07-07, estate F76F97BC): the WWDC26 -// adoption path (App Intents 2027 wave, Core AI, SpotlightSearchTool) is 27+. -// Relative paths are ../../ — this package lives in apps/, siblings of packages/. import PackageDescription let package = Package( name: "Mootx01-App", - platforms: [ - .macOS("27.0"), - .iOS("27.0"), - ], + platforms: [.macOS("27.0")], products: [ - .library(name: "MootGateway", targets: ["MootGateway"]), - .library(name: "GatewayUI", targets: ["GatewayUI"]), + .library(name: "MootCommunityGateway", targets: ["MootCommunityGateway"]), + .library(name: "MootCommunityUI", targets: ["MootCommunityUI"]), + .library(name: "MootCommunityUITestSupport", targets: ["MootCommunityUITestSupport"]), ], dependencies: [ .package(name: "AriaMcpKit", path: "../../packages/kits/AriaMcpKit"), - .package(name: "GeniusLocusKit", path: "../../packages/kits/GeniusLocusKit"), - .package(name: "LocusKit", path: "../../packages/kits/LocusKit"), - .package(name: "PersistenceKit", path: "../../packages/kits/PersistenceKit"), - // MootIntentKit owns the intent surface; MootBridge conforms to its - // MootToolCalling protocol so the kit never reaches substrate internals. - .package(name: "MootIntentKit", path: "../../packages/apple/MootIntentKit"), - .package(name: "MootFoundationModelsKit", path: "../../packages/apple/MootFoundationModelsKit"), - // Sync lives in ConvergenceKit (CloudKitSyncEngine / NoSyncEngine behind - // the SyncEngine protocol) — the app wires it, it does not reimplement it. - .package(name: "ConvergenceKit", path: "../../packages/kits/ConvergenceKit"), - .package(name: "WorkPacketKit", path: "../../packages/kits/WorkPacketKit"), ], targets: [ .target( - name: "MootGateway", + name: "MootCommunityGateway", dependencies: [ - .product(name: "AriaMCP", package: "AriaMcpKit"), - .product(name: "GeniusLocusKit", package: "GeniusLocusKit"), - .product(name: "LocusKit", package: "LocusKit"), - .product(name: "PersistenceKit", package: "PersistenceKit"), - .product(name: "PersistenceKitInMemory", package: "PersistenceKit"), - .product(name: "PersistenceKitSQLite", package: "PersistenceKit"), - .product(name: "MootIntentKit", package: "MootIntentKit"), - .product(name: "ConvergenceKit", package: "ConvergenceKit"), - .product(name: "ConvergenceKitCloudKit", package: "ConvergenceKit"), - // FED-OD-3: QR pairing ceremony uses ConvergenceKitFederation types - // (PairingProposal, PairingAcceptance, HyperplaneFamilySpec, etc.) - .product(name: "ConvergenceKitFederation", package: "ConvergenceKit"), + .product(name: "AriaMCPWire", package: "AriaMcpKit"), ], - path: "Sources/MootGateway" + path: "Sources/MootCommunityGateway" ), .target( - name: "GatewayUI", - dependencies: [ - "MootGateway", - .product(name: "MootIntentKit", package: "MootIntentKit"), - .product(name: "MootFoundationModelsKit", package: "MootFoundationModelsKit"), - // FED-OD-3: QR pairing views reference ConvergenceKitFederation types - // (LocalIdentity, HyperplaneFamilySpec) passed in from the app layer. - .product(name: "ConvergenceKitFederation", package: "ConvergenceKit"), - // FAB5-I3: PacketListView, PacketDetailView, LineageView consume - // WorkPacket, WorkPacketStore, LineageGraph. - .product(name: "WorkPacketKit", package: "WorkPacketKit"), - ], - path: "Sources/GatewayUI" + name: "MootCommunityUI", + dependencies: ["MootCommunityGateway"], + path: "Sources/MootCommunityUI" ), - .testTarget( - name: "MootGatewayTests", + .target( + name: "MootCommunityUITestSupport", dependencies: [ - "MootGateway", - .product(name: "MootIntentKit", package: "MootIntentKit"), - // A2 integration tests start the real ARIA HTTP server in-process - // (HTTPServer from AriaMCP) to verify HTTPTransport against the live - // wire. These are the only imports beyond MootGateway in the test - // target; they are unavoidable for the in-process server harness. - .product(name: "AriaMCP", package: "AriaMcpKit"), - .product(name: "GeniusLocusKit", package: "GeniusLocusKit"), - .product(name: "LocusKit", package: "LocusKit"), - .product(name: "PersistenceKitInMemory", package: "PersistenceKit"), - .product(name: "ConvergenceKit", package: "ConvergenceKit"), - .product(name: "ConvergenceKitNone", package: "ConvergenceKit"), - // P5-M2 push nudge tests: need @testable access to - // CloudKitSyncEngine.cloudKitZoneName (internal method). - .product(name: "ConvergenceKitCloudKit", package: "ConvergenceKit"), - // FED-OD-3: QR pairing ceremony tests use ConvergenceKitFederation types. - .product(name: "ConvergenceKitFederation", package: "ConvergenceKit"), + "MootCommunityGateway", + "MootCommunityUI", + .product(name: "AriaMCPWire", package: "AriaMcpKit"), ], - path: "Tests/MootGatewayTests" + path: "Tests/MootCommunityUITestSupport" ), - // A-4: unit tests for AppModel.lastLoggedID() — verifies the capture-log - // UUID parse without needing a live bridge (intentRunLog is populated - // directly). GatewayUI is a SwiftUI layer so this target is macOS-only; - // the function under test has no platform-specific behavior. .testTarget( - name: "GatewayUITests", - dependencies: [ - "GatewayUI", - "MootGateway", - .product(name: "MootIntentKit", package: "MootIntentKit"), - // FAB5-I3: PacketViewsTests constructs WorkPacket fixtures directly. - .product(name: "WorkPacketKit", package: "WorkPacketKit"), - ], - path: "Tests/GatewayUITests" + name: "CommunityBoundaryTests", + dependencies: ["MootCommunityUI", "MootCommunityGateway"], + path: "Tests/CommunityBoundaryTests" ), ] ) diff --git a/apps/Mootx01-App/README.md b/apps/Mootx01-App/README.md index 1f28d4ac3..66f8795c3 100644 --- a/apps/Mootx01-App/README.md +++ b/apps/Mootx01-App/README.md @@ -1,228 +1,61 @@ -# MOOTx01-App +# MOOTx01 Community for macOS -MOOTx01-App is the native Apple presentation layer for MOOTx01 on macOS, -iOS, and iPadOS. It hosts a local estate, projects ARIA onto Apple system -surfaces, and gives developers a working integration shell for capture, -recall, on-device intelligence, sync, local-network serving, and federation. +MOOTx01 Community is the open desktop application for a single-owner local +estate. It provides Capture, Recall, Review, graph and engine visibility, +Product Dock attachment, and an explicitly enabled portable MCP listener. -This directory is the development app on `develop/1.1.x`. It is distinct from -[`moot-mgr`](../moot-mgr/README.md), which operates and observes the resident -headless daemon. - -Start with this guide for building and using the app. Use the -[`MOOTx01-App specification`](../../docs/reference/MOOTX01_APP_SPEC.md) for -the complete behavioral contract and the -[`ARIA-to-Apple mapping`](LEXICON_TO_APPLE_MAPPING.md) for every verb and -system surface. - -## What the app provides - -| Surface | Current development behavior | -|---|---| -| Capture and recall | Files and searches the app's durable local estate through the ARIA tool surface. Capture includes sensitivity and exportability. | -| Intelligence | Uses Apple Foundation Models through `MootFoundationModelsKit`. Recall is treated as untrusted data. Capture requires one-shot authorization. | -| Siri, Shortcuts, and App Intents | Six caller-driven verbs are implemented and tested. System registration occurs when the generated Xcode app bundle is built and installed. | -| Share Sheet | The extension writes to a durable app-group spool. The host app drains it into the estate at launch and foreground activation. | -| Spotlight and recall widget | Derived, public-only projections. Neither surface is canonical storage, and the widget never opens the estate. | -| Calendar and birthday miners | Disabled by default. Consent can be requested only from an attended Mine Now operation. | -| CloudKit sync | User opt-in and disabled by default. Normal and elevated rows may sync; restricted and secret rows are blocked by the storage wrapper. | -| Portable LAN MCP | Owner-presence credential, read-only tool allowlist, and public/exportable recall. Serving is on-power by default and foreground-bound on iOS. | -| On-demand federation | Off by default. The F1 surface includes Bonjour discovery, QR/SAS pairing, hardware-gated UWB proximity, a Balanced session posture, and explicit session teardown. | -| macOS host controls | Embedded hosting, menu-bar headless mode, and supervision of a separate `aria-mcp` process over stdio. | - -## How it fits together +The Community application is physically composed from two open library modules +and its executable target: ```text -Siri / Shortcuts / App Intents / SwiftUI / callback URLs -Share Sheet spool / Spotlight projection / recall widget - | - MootGateway - | - MootBridge - | - in-process ARIA dispatcher - | - GeniusLocusKit estate - | - LocusKit + CorpusKit + supporting kits +MootCommunityGateway → MootCommunityUI → CommunityApp ``` -The app is an envelope around the platform-neutral engine: - -- `MootGateway` owns Apple-side orchestration, host selection, sync controls, - LAN serving, miners, and federation UI coordination. -- `MootBridge` is the one gateway into the ARIA dispatcher. Apple code does - not bypass it to write estate tables directly. -- `AriaMcpKit`, GeniusLocusKit, and the substrate kits remain the - Swift/Rust-parity engine. -- App extensions exchange bounded projections or queued capture requests - through the app group. They do not become additional estate hosts. - -## Estate and host ownership +It contains no iPhone or iPad app, App Intents or Shortcuts catalog, CloudKit +sync, personal federation, Calendar or Contacts miners, Foundation Models +integration, widget, or share extension. Those capabilities belong to other +product editions and are absent from this source and dependency graph. -The application follows one rule: **one estate, one host**. - -- **Embedded host:** the app opens the estate in-process. This is the normal - path on macOS, iOS, and iPadOS. -- **Managed daemon:** on macOS the app can spawn and supervise the separate - `aria-mcp` binary over stdio. The current panel proves process supervision - and `tools/list` against the daemon's own estate. -- **Direct handoff:** transferring the app's already-open estate to that - daemon still requires an app-side estate close operation. The current UI - names this boundary and does not claim that the handoff is complete. - -With no test override, the app opens: - -```text -/mootx01/mootx01.sqlite -``` +## Verify and test -The GUI, cold-launched App Intents, callback URLs, miners, share-inbox drain, -and sync driver all resolve through the same process-wide `GatewayRuntime`. - -## The application tabs - -| Tab | Purpose | -|---|---| -| Capture | File content with location, sensitivity, and exportability. | -| Recall | Search the estate, with an optional public/exportable-only filter. | -| Intelligence | Ask the on-device model to use estate recall, with explicit one-shot capture permission. | -| The Top | Inspect the ARIA tool surface grouped by memory, graph, vault, estate, and reasoning roles. | -| Apple Surfaces | Exercise the six App Intent verbs in-process and inspect their run log. | -| Edges | See which adapters are live, registered, seamed, or deliberately unavailable. | -| Engine | Inspect the embedded host, sync control, LAN serving, daemon supervision, and discovery. | -| Federation | Control visibility, pair estates, choose the available posture, start a timed session, and end it. | -| Miners | Enable Calendar or Birthday mining, select cadence, and run an attended ingest. | - -On macOS, menu-bar mode is enabled by default so the embedded engine can stay -alive after the last window closes. - -## Build and run - -Requirements: - -- Xcode with the macOS 27 and iOS 27 SDKs -- Swift 6.2 -- `xcodegen` -- Apple signing and entitlements for device-only capabilities - -Run the Swift package tests from the repository root: +From the repository root: ```sh -swift test --package-path apps/Mootx01-App +apps/Mootx01-App/scripts/verify-community-boundary.sh ``` -Generate the app project: +The verifier scans Swift imports and conditional imports, checks the exact +Community dependency graph, builds the Community UI, and runs the complete +Community intent and application test suites. It uses the published lockfiles +with automatic package resolution disabled, so verification cannot alter the +reviewed dependency set. + +Generate the macOS project with XcodeGen: ```sh cd apps/Mootx01-App xcodegen generate -``` - -`project.yml` generates `Mootx01-App.xcodeproj`. Build either application -target: - -```sh xcodebuild \ -project Mootx01-App.xcodeproj \ - -scheme Mootx01-macOS \ + -scheme Mootx01-Community-macOS \ -destination 'platform=macOS' \ build - -xcodebuild \ - -project Mootx01-App.xcodeproj \ - -scheme Mootx01-iOS \ - -destination 'generic/platform=iOS Simulator' \ - build ``` -The Swift package contains the reusable `MootGateway` and `GatewayUI` -libraries. The runnable applications are Xcode targets because App Intents, -extensions, entitlements, and system registration require a real app bundle. +`community-export.json` records the reviewed publication boundary that +produced this tree. Paths in its `forbidden` list must remain absent. -### Build the managed server +## Release artifact -The macOS Engine panel can supervise the reference `aria-mcp` executable: +Run the release command from the projected public CE checkout. A full release +requires a notarytool keychain profile and fails before notarization unless the +embedded Developer ID profile authorizes the Community/daemon App Group: ```sh -swift build \ - --package-path apps/aria-mcp-server \ - -c release \ - --product aria-mcp +apps/Mootx01-App/scripts/release-community.sh \ + --output-root /absolute/external/build/path \ + --notary-keychain-profile PROFILE ``` -Point the panel at: - -```text -apps/aria-mcp-server/.build/release/aria-mcp -``` - -## Configuration and external prerequisites - -Several development surfaces are intentionally default-closed: - -- **CloudKit:** the user must enable sync. The iCloud container - `iCloud.com.codedaptive.mootx01` and an available iCloud account are also - required. -- **Federation discovery:** visibility defaults to Off. Always-visible is - meaningful only for a resident Mac host. -- **LAN MCP:** starting or revealing the bearer token requires device-owner - presence. The remote surface is read-only and public-only. -- **Miners:** every source ships disabled. Merely opening the Miners tab does - not read Calendar or Contacts and does not trigger consent. -- **Siri and Shortcuts:** implementations can run in-process under tests, but - system registration needs the generated app to be installed. - -Provisioning identifiers, signing, app groups, CloudKit, and TestFlight gates -are covered by the -[`Apple provisioning runbook`](../../docs/status/APPLE_PROVISIONING_RUNBOOK.md). - -## Security and privacy boundaries - -- The estate is SQLCipher encrypted at rest. -- Exportability is the serve-out gate for Spotlight, widgets, LAN recall, and - other outbound projections. -- CloudKit and federation apply a sensitivity ceiling. Restricted and secret - rows are not placed on those transports. -- Recalled content sent to a language model is bounded as untrusted data and - cannot authorize its own capture. -- Callback URLs use a verb allowlist and do not auto-open unapproved return - schemes. -- Share and widget extensions never open the estate database. -- The privacy manifest declares required-reason APIs and no tracking. - -## Current boundaries - -- Generic outbound MCP federation through `MootEstateClient.fetch` is still - guarded and throws. Its local fold-in half exists. This is separate from - the implemented ConvergenceKit F1 on-demand federation session. -- The standalone daemon does not yet advertise the `_mootx01._tcp` service, - so app-side daemon discovery remains a seam. -- iOS cannot host a persistent subprocess. Its embedded estate and LAN - listener exist only while the app receives execution time. -- CloudKit code remains inert until its container and account prerequisites - are satisfied. -- The F1 federation UI exposes the Balanced posture. Unbuilt postures remain - visibly locked. - -## Source map - -| Path | Responsibility | -|---|---| -| `App/` | Application entry point, delegates, App Shortcuts provider, entitlements, and privacy manifest. | -| `Sources/GatewayUI/` | Shared SwiftUI views and application model. | -| `Sources/MootGateway/` | Estate bridge, runtime, host controls, transports, sync, miners, LAN server, and federation. | -| `ShareExtension/` | UI-less capture handoff into the app-group spool. | -| `RecallWidget/` | Public-only derived recall snapshot presentation. | -| `Tests/` | Gateway and UI policy tests, including negative security boundaries. | -| `UITests/` | App Intents metadata and cold-launch integration tests. | -| `project.yml` | Regenerable macOS, iOS, widget, share-extension, and UI-test project definition. | - -## Further documentation - -- [`MOOTx01-App specification`](../../docs/reference/MOOTX01_APP_SPEC.md) -- [`ARIA lexicon to Apple mapping`](LEXICON_TO_APPLE_MAPPING.md) -- [`Portable LAN server decision`](../../docs/decisions/DECISION_MOOTX01_APP_PORTABLE_LAN_SERVER_2026-07-11.md) -- [`On-demand federation decision`](../../docs/decisions/DECISION_FEDERATION_ONDEMAND_LAN_PROXIMITY_2026-07-18.md) -- [`Apple provisioning runbook`](../../docs/status/APPLE_PROVISIONING_RUNBOOK.md) -- [`System engineering reference`](../../docs/engineering/SYSTEM_ENGINEERING_REFERENCE.md) +Use `--prepare-only` instead of `--notary-keychain-profile` to produce the +verified notarization input without submitting it. diff --git a/apps/Mootx01-App/RecallWidget/MootRecallWidget.swift b/apps/Mootx01-App/RecallWidget/MootRecallWidget.swift deleted file mode 100644 index 6b0f7affb..000000000 --- a/apps/Mootx01-App/RecallWidget/MootRecallWidget.swift +++ /dev/null @@ -1,103 +0,0 @@ -import WidgetKit -import SwiftUI -import MootIntentKit - -// MARK: - MootRecallWidget (Tier 3 — recall on the home screen / desktop) -// -// Renders the derived projection the app maintains in the app-group -// container (WidgetSnapshotStore). This process never opens the estate — -// see the store's header for the one-estate-one-host rationale and the -// export gate (only explicitly public drawers can be in the projection). - -@main -struct MootRecallWidgetBundle: WidgetBundle { - var body: some Widget { - MootRecallWidget() - } -} - -struct MootRecallWidget: Widget { - var body: some WidgetConfiguration { - StaticConfiguration(kind: "MootRecallWidget", provider: SnapshotProvider()) { entry in - RecallWidgetView(entry: entry) - .containerBackground(.fill.tertiary, for: .widget) - } - .configurationDisplayName(Text(String(localized: "widget.recall.title", defaultValue: "Recent Memories"))) - .description(Text(String(localized: "widget.recall.description", defaultValue: "Your most recent public memories from the MOOT."))) - .supportedFamilies([.systemSmall, .systemMedium]) - } -} - -// MARK: - Timeline - -struct SnapshotTimelineEntry: TimelineEntry { - let date: Date - let entries: [WidgetSnapshot.Entry] -} - -struct SnapshotProvider: TimelineProvider { - - private func currentEntry() -> SnapshotTimelineEntry { - let snapshot = (try? WidgetSnapshotStore.groupStore())?.read() - return SnapshotTimelineEntry( - date: snapshot?.updatedAt ?? Date(), - entries: snapshot?.entries ?? []) - } - - func placeholder(in context: Context) -> SnapshotTimelineEntry { - SnapshotTimelineEntry(date: Date(), entries: [ - .init(id: UUID().uuidString, - content: String(localized: "widget.recall.placeholder", defaultValue: "A memory you filed"), - room: String(localized: "widget.recall.placeholder.room", defaultValue: "workspace")), - ]) - } - - func getSnapshot(in context: Context, completion: @escaping (SnapshotTimelineEntry) -> Void) { - completion(currentEntry()) - } - - func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { - // The app pushes reloads on every projection refresh; the 30-minute - // re-read is only a backstop for a stale timeline after reboot. - completion(Timeline( - entries: [currentEntry()], - policy: .after(Date(timeIntervalSinceNow: 30 * 60)))) - } -} - -// MARK: - View - -struct RecallWidgetView: View { - @Environment(\.widgetFamily) private var family - let entry: SnapshotTimelineEntry - - private var visibleEntries: [WidgetSnapshot.Entry] { - Array(entry.entries.prefix(family == .systemSmall ? 2 : 4)) - } - - var body: some View { - if visibleEntries.isEmpty { - Text(String(localized: "widget.recall.empty", - defaultValue: "No public memories yet. Mark a capture public to see it here.")) - .font(.caption) - .foregroundStyle(.secondary) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) - } else { - VStack(alignment: .leading, spacing: 6) { - ForEach(visibleEntries) { item in - VStack(alignment: .leading, spacing: 1) { - Text(item.content) - .font(.caption) - .lineLimit(family == .systemSmall ? 2 : 1) - Text(item.room) - .font(.caption2) - .foregroundStyle(.secondary) - } - } - Spacer(minLength: 0) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - } - } -} diff --git a/apps/Mootx01-App/RecallWidget/Mootx01-Widget-iOS.entitlements b/apps/Mootx01-App/RecallWidget/Mootx01-Widget-iOS.entitlements deleted file mode 100644 index cc765de77..000000000 --- a/apps/Mootx01-App/RecallWidget/Mootx01-Widget-iOS.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.application-groups - - group.com.codedaptive.mootx01 - - - diff --git a/apps/Mootx01-App/RecallWidget/Mootx01-Widget-macOS.entitlements b/apps/Mootx01-App/RecallWidget/Mootx01-Widget-macOS.entitlements deleted file mode 100644 index b617fba66..000000000 --- a/apps/Mootx01-App/RecallWidget/Mootx01-Widget-macOS.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.application-groups - - group.com.codedaptive.mootx01 - - - diff --git a/apps/Mootx01-App/ShareExtension/Mootx01-Share-iOS.entitlements b/apps/Mootx01-App/ShareExtension/Mootx01-Share-iOS.entitlements deleted file mode 100644 index cc765de77..000000000 --- a/apps/Mootx01-App/ShareExtension/Mootx01-Share-iOS.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.application-groups - - group.com.codedaptive.mootx01 - - - diff --git a/apps/Mootx01-App/ShareExtension/Mootx01-Share-macOS.entitlements b/apps/Mootx01-App/ShareExtension/Mootx01-Share-macOS.entitlements deleted file mode 100644 index b617fba66..000000000 --- a/apps/Mootx01-App/ShareExtension/Mootx01-Share-macOS.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.application-groups - - group.com.codedaptive.mootx01 - - - diff --git a/apps/Mootx01-App/ShareExtension/ShareViewController.swift b/apps/Mootx01-App/ShareExtension/ShareViewController.swift deleted file mode 100644 index 9116d7400..000000000 --- a/apps/Mootx01-App/ShareExtension/ShareViewController.swift +++ /dev/null @@ -1,103 +0,0 @@ -import Foundation -import UniformTypeIdentifiers -import MootIntentKit -#if os(iOS) -import UIKit -#elseif os(macOS) -import AppKit -#endif - -// MARK: - ShareViewController (A4b — the Share-Sheet capture target) -// -// UI-less by design: harvest the shared text/URL, spool it, complete. The -// The extension process never opens the estate; one estate has one host. -// it writes one JSON file into the app-group ShareInbox and the host app -// drains that spool through CaptureSink at launch, foregrounding, and each -// mining tick. Content shared while the host is not running is simply -// captured at the next host run — the spool is durable. -// -// Failure posture: no group container or no usable text → cancelRequest with -// the error, so the system's share UI reports failure instead of silently -// dropping the user's content. - -@objc(ShareViewController) -final class ShareViewController: PlatformViewController { - - private var processed = false - - #if os(iOS) - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - processOnce() - } - #elseif os(macOS) - override func loadView() { - view = NSView() - } - - override func viewDidAppear() { - super.viewDidAppear() - processOnce() - } - #endif - - private func processOnce() { - guard !processed else { return } - processed = true - Task { - await handleShare() - } - } - - private func handleShare() async { - guard let text = await harvestText(), !text.isEmpty else { - cancel(reason: "No text or link found in the shared content.") - return - } - do { - let spool = try ShareInboxSpool.groupSpool() - try await spool.enqueue(.init(text: text, location: "shared")) - extensionContext?.completeRequest(returningItems: nil) - } catch { - cancel(reason: "\(error)") - } - } - - /// First URL wins (a shared link's text is usually the page title — - /// the URL is the durable fact); plain text is the fallback. - private func harvestText() async -> String? { - let items = (extensionContext?.inputItems ?? []).compactMap { $0 as? NSExtensionItem } - let providers = items.flatMap { $0.attachments ?? [] } - - for provider in providers where provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { - if let raw = try? await provider.loadItem(forTypeIdentifier: UTType.url.identifier) { - if let url = raw as? URL { return url.absoluteString } - if let data = raw as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) { - return url.absoluteString - } - } - } - for provider in providers where provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) { - if let raw = try? await provider.loadItem(forTypeIdentifier: UTType.plainText.identifier) { - if let text = raw as? String { return text } - if let data = raw as? Data, let text = String(data: data, encoding: .utf8) { - return text - } - } - } - return nil - } - - private func cancel(reason: String) { - extensionContext?.cancelRequest(withError: NSError( - domain: "com.codedaptive.mootx01.share", - code: 1, - userInfo: [NSLocalizedDescriptionKey: reason])) - } -} - -#if os(iOS) -typealias PlatformViewController = UIViewController -#elseif os(macOS) -typealias PlatformViewController = NSViewController -#endif diff --git a/apps/Mootx01-App/Sources/GatewayUI/AdvancedModeToggle.swift b/apps/Mootx01-App/Sources/GatewayUI/AdvancedModeToggle.swift deleted file mode 100644 index bc3dd51bc..000000000 --- a/apps/Mootx01-App/Sources/GatewayUI/AdvancedModeToggle.swift +++ /dev/null @@ -1,113 +0,0 @@ -import SwiftUI -import MootGateway - -// MARK: - AdvancedModeToggleView (FAB5-FR Part 2) -// -// Full-screen Settings tab for both Standard and Advanced profiles. -// Shows the iCloud Sync section (same key as SettingsView — single source of -// truth; changes in either view are immediately visible in both) and the -// Advanced Mode toggle. Standard users reach iCloud Sync here; Advanced users -// also see it in the Engine tab's SyncTileView. - -struct AdvancedModeToggleView: View { - @Bindable var model: AppModel - - // Same UserDefaults key as SettingsView and SyncTileView (SyncPolicy.masterEnabledKey). - // All three views share the key — changes in any are immediately reflected in the others. - @AppStorage(SyncPolicy.masterEnabledKey) private var masterEnabled = false - - var body: some View { - NavigationStack { - Form { - syncSection - advancedModeSection - } - .formStyle(.grouped) - .navigationTitle(String(localized: "advancedmode.nav.title", defaultValue: "Settings")) - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - } - } - - // MARK: iCloud Sync section - // Mirrors SettingsView.syncSection — same key, same driver call, same copy. - // Duplicated here so Standard-profile users (who never see SettingsView on - // iOS, since the gear button is on the Engine tab that moves behind Advanced) - // can still reach the sync master switch. - - private var syncSection: some View { - Section { - Toggle(isOn: $masterEnabled) { - Label { - VStack(alignment: .leading, spacing: 2) { - Text(String(localized: "settings.sync.toggle.label", - defaultValue: "iCloud Sync")) - Text(masterEnabled - ? String(localized: "settings.sync.status.on", - defaultValue: "Normal and Elevated memories sync across your Apple devices.") - : String(localized: "settings.sync.status.off", - defaultValue: "Memories stay on this device.")) - .font(.caption) - .foregroundStyle(.secondary) - } - } icon: { - Image(systemName: "icloud") - .accessibilityHidden(true) - } - } - .accessibilityLabel(String(localized: "settings.sync.toggle.a11y.label", - defaultValue: "iCloud Sync")) - .accessibilityHint(String(localized: "settings.sync.toggle.a11y.hint", - defaultValue: "When on, Normal and Elevated memories sync across your Apple devices via iCloud.")) - .onChange(of: masterEnabled) { _, newValue in - Task { - await MootSyncDriver.shared.configure(SyncPolicy.config(enabled: newValue)) - if newValue { - _ = await MootSyncDriver.shared.syncNow() - } - } - } - } header: { - Text(String(localized: "settings.sync.section.header", - defaultValue: "iCloud Sync")) - } footer: { - Text(String(localized: "settings.sync.section.footer", - defaultValue: "Restricted and Secret memories never leave this device, regardless of this setting.")) - } - } - - // MARK: Advanced Mode section - - private var advancedModeSection: some View { - Section { - Toggle(isOn: $model.isAdvancedMode) { - Label { - VStack(alignment: .leading, spacing: 2) { - Text(String(localized: "advancedmode.toggle.label", defaultValue: "Advanced Mode")) - Text( - model.isAdvancedMode - ? String(localized: "advancedmode.toggle.subtitle.on", - defaultValue: "All engineering tabs are visible.") - : String(localized: "advancedmode.toggle.subtitle.off", - defaultValue: "Simplified view: Capture, Recall, Intelligence, and Settings.") - ) - .font(.caption) - .foregroundStyle(.secondary) - } - } icon: { - Image(systemName: "slider.horizontal.3") - .accessibilityHidden(true) - } - } - .accessibilityLabel(String(localized: "advancedmode.toggle.a11y.label", defaultValue: "Advanced Mode")) - .accessibilityHint(String(localized: "advancedmode.toggle.a11y.hint", - defaultValue: "When on, engineering tabs — The Top, Edges, Engine, and more — become visible.")) - } header: { - Text(String(localized: "advancedmode.section.header", defaultValue: "Interface")) - } footer: { - Text(String(localized: "advancedmode.section.footer", - defaultValue: "Advanced Mode shows The Top, Apple Surfaces, Edges, Engine, Federation, and Miners. Designed for developers and power users.")) - } - } -} diff --git a/apps/Mootx01-App/Sources/GatewayUI/AppModel.swift b/apps/Mootx01-App/Sources/GatewayUI/AppModel.swift deleted file mode 100644 index f1c64c46f..000000000 --- a/apps/Mootx01-App/Sources/GatewayUI/AppModel.swift +++ /dev/null @@ -1,322 +0,0 @@ -import Foundation -import SwiftUI -import MootGateway -import MootIntentKit // intent types: CaptureDrawerIntent, RecallDrawerIntent, CallerVerbIntents -import AriaMCP // JSONValue (for reading tools/list schemas) -import OSLog - -// MARK: - AppModel -// -// The single observable model behind the five tabs, shared verbatim by the -// macOS executable and the iOS app. It owns no substrate logic — it drives -// `MootBridge` (through GatewayRuntime so the App Intent shells share the same -// estate) and holds the rendered results. Every action keeps the full -// GatewayCall (request + response JSON) so the views can show the wire, which -// is the whole point: see the top-level communication. - -/// One tool descriptor pulled from `tools/list`, grouped for display. -struct ToolDescriptor: Identifiable, Sendable { - var id: String { name } - let name: String - let description: String - let schemaPretty: String - let group: String -} - -@MainActor -@Observable -public final class AppModel { - - // First-run flag — shown once; UserDefaults-backed so it survives app restarts. - // Default false (not completed). Set to true when onboarding is dismissed or skipped. - var hasCompletedOnboarding: Bool = UserDefaults.standard.bool(forKey: "com.mootx01.gateway.hasCompletedOnboarding") { - didSet { UserDefaults.standard.set(hasCompletedOnboarding, forKey: "com.mootx01.gateway.hasCompletedOnboarding") } - } - - // Tab profile — false = Standard (Capture/Recall/Intelligence/Settings only); - // true = Advanced (Standard + engineering tabs). Default Standard. - var isAdvancedMode: Bool = UserDefaults.standard.bool(forKey: "com.mootx01.gateway.isAdvancedMode") { - didSet { UserDefaults.standard.set(isAdvancedMode, forKey: "com.mootx01.gateway.isAdvancedMode") } - } - - // Lifecycle - public private(set) var bridge: MootBridge? - var statusLine: String = "Not attached" - var databasePath: String? - - // Capture tab - var captureContent: String = "MOOTx01 projects ARIA onto Apple surfaces." - var captureLocation: String = "gateway" - var captureSensitivity: String = "normal" - /// Exportability of the next capture: "private" (default) or "public". - /// "public" drawers are returned by filter:exportable recall (§6.2 serve-out gate). - var captureExportability: String = "private" - var lastCaptureCall: GatewayCall? - - // Recall tab - var recallQuery: String = "ARIA" - var recallPublicOnly: Bool = false - var lastRecallCall: GatewayCall? - - // The Top tab - var tools: [ToolDescriptor] = [] - - // Apple Surfaces tab — results of running an intent shell in-process - var intentRunLog: [String] = [] - - let sensitivityOptions = ["normal", "elevated", "restricted", "secret"] - /// The two exportability choices; raw values match the moot_file_memory tool vocabulary. - let exportabilityOptions = ["private", "public"] - - public init() {} - - /// Attach the shared bridge over a durable SQLite estate, then preload the - /// tool surface for "The Top". - public func start() async { - do { - let attached = try await GatewayRuntime.shared.bridge() - bridge = attached - databasePath = attached.databasePath - statusLine = "Attached · \(attached.serverName) · \(attached.databasePath ?? "in-memory")" - await loadTools() - await seedIfEmpty(attached) - } catch { - statusLine = "Attach failed: \(error)" - } - } - - /// Ship-with-sample-data: on first launch (empty estate) file a few sample - /// drawers so the app has content to show. A prebuilt bundled `.sqlite` - /// would achieve the same; seeding on first launch is simpler and keeps the - /// estate in the user's own container. - private func seedIfEmpty(_ bridge: MootBridge) async { - let probe = await bridge.callToolFull("moot_memory_search", arguments: ["query": .string("MOOTx01")]) - guard probe.text.contains("found 0") else { return } - let samples = [ - ("MOOTx01 projects ARIA onto Apple surfaces — Siri, Spotlight, Shortcuts.", "gateway"), - ("The engine is the clean, Rust-mirrored server; the app envelopes it.", "architecture"), - ("One estate, one host: embedded server-in-app, or a handed-off daemon.", "architecture"), - ] - for (content, room) in samples { - _ = await bridge.callToolFull("moot_file_memory", arguments: [ - "content": .string(content), "location": .string(room), - ]) - } - } - - /// `/mootx01/mootx01.sqlite`. Cross-platform: - /// on macOS this is ~/Library/Application Support/…; on iOS it is the app - /// sandbox container, so each install gets its own estate. - /// (`homeDirectoryForCurrentUser` is macOS-only, so it is not used here.) - static func defaultDatabaseURL() -> URL { - EstateConfigurationResolver.defaultDatabaseURL() - } - - // MARK: Capture / Recall - - func doCapture() async { - guard let bridge else { return } - lastCaptureCall = await bridge.callToolFull("moot_file_memory", arguments: [ - "content": .string(captureContent), - "location": .string(captureLocation), - "sensitivity": .string(captureSensitivity), - // exportability: "private" (default, omitting is equivalent) or "public". - // "public" lets this drawer surface via filter:exportable recall (A4a serve-out gate). - "exportability": .string(captureExportability), - ]) - } - - func doRecall() async { - guard let bridge else { return } - var arguments: [String: JSONValue] = ["query": .string(recallQuery)] - if recallPublicOnly { - arguments["filter"] = .string("exportable") - } - lastRecallCall = await bridge.callToolFull("moot_memory_search", arguments: arguments) - } - - // MARK: The Top — tool surface - - func loadTools() async { - guard let bridge else { return } - let list = await bridge.toolsList() - let entries = list.objectValue?["tools"]?.arrayValue ?? [] - tools = entries.compactMap { entry in - guard let object = entry.objectValue, - let name = object["name"]?.stringValue else { return nil } - let description = object["description"]?.stringValue ?? "" - let schema = object["inputSchema"].map(MootBridge.pretty) ?? "{}" - return ToolDescriptor( - name: name, - description: description, - schemaPretty: schema, - group: Self.group(for: name) - ) - } - } - - private static func group(for name: String) -> String { - if name.hasPrefix("moot_lens_") { return "Reasoning lenses" } - if name.hasPrefix("moot_vault_") { return "Vault" } - if name.contains("fact") { return "Knowledge graph" } - if name.contains("connection") || name.contains("link") { return "Connections" } - if name.contains("journal") { return "Journal" } - if name.contains("estate") { return "Estate" } - if name.contains("lens") || name.contains("synthesize") || name.contains("migration") { return "Recipes" } - return "Core memory" - } - - /// Tool groups in a stable display order. - var toolGroups: [(String, [ToolDescriptor])] { - let order = ["Core memory", "Connections", "Knowledge graph", "Journal", "Estate", "Recipes", "Reasoning lenses", "Vault"] - let grouped = Dictionary(grouping: tools, by: \.group) - return order.compactMap { key in - guard let items = grouped[key], !items.isEmpty else { return nil } - return (key, items) - } - } - - // MARK: Apple Surfaces — run an intent in-process - - /// Invoke any of the six caller-driven verb intents against the live shared - /// estate. All six run in-process today; system registration (Siri, - /// Shortcuts catalog) requires the Xcode app bundle packaging step. - /// The structural verbs (reanchor/mutate/withdraw/expunge) use the most - /// recently filed drawer's id from the run log, falling back to a sentinel - /// value when none is available (the substrate will reject the sentinel and - /// report the refusal as a logged error, which is the correct behavior). - func runIntent(_ verb: String) async { - do { - switch verb { - case "capture": - // Route through the bridge directly (same path CaptureDrawerIntent - // uses internally) so the tool-response text is available here to - // extract the drawer id. The response first line is always - // "filed memory " (ToolDispatch.runFileMemory), which lets - // subsequent structural verbs (reanchor/mutate/withdraw/expunge) - // resolve the id via lastLoggedID() without a sentinel fall-back. - guard let b = bridge else { throw IntentToolError.substrateRefused("no bridge attached") } - let captureResult = await b.callTool("moot_file_memory", arguments: [ - "content": .string("Captured via the CaptureDrawerIntent at \(timestamp())."), - "location": .string("apple-surfaces"), - ]) - if captureResult.isError { throw IntentToolError.substrateRefused(captureResult.text) } - // Log the first line of the result ("filed memory ") verbatim - // so lastLoggedID() can extract the UUID via a standard UUID pattern. - let resultFirstLine = captureResult.text.components(separatedBy: "\n").first ?? captureResult.text - intentRunLog.insert("capture: \(resultFirstLine) — filed into the live estate.", at: 0) - - case "recall": - _ = try await RecallDrawerIntent(query: "apple-surfaces").perform() - intentRunLog.insert("recall: RecallDrawerIntent.perform() ran — results returned as IntentResult.", at: 0) - - case "reanchor": - // Uses the last-captured id extracted from the run log, or a - // sentinel that the substrate will reject with a clear error. - let id = lastLoggedID() ?? "no-id-captured-yet" - _ = try await ReanchorDrawerIntent(id: id, location: "apple-surfaces-reanchored").perform() - intentRunLog.insert("reanchor: ReanchorDrawerIntent.perform() ran for id=\(id).", at: 0) - - case "mutate": - let id = lastLoggedID() ?? "no-id-captured-yet" - _ = try await MutateDrawerIntent(id: id, mutation: "confirm").perform() - intentRunLog.insert("mutate: MutateDrawerIntent.perform() ran (confirm) for id=\(id).", at: 0) - - case "withdraw": - let id = lastLoggedID() ?? "no-id-captured-yet" - _ = try await WithdrawDrawerIntent(id: id).perform() - intentRunLog.insert("withdraw: WithdrawDrawerIntent.perform() ran for id=\(id).", at: 0) - - case "expunge": - // Runs with confirmed=false so the view-layer test does not - // permanently erase data. The substrate must refuse; the refusal - // is logged to confirm the confirmation guard is active. - // To actually erase, the Shortcuts flow prompts confirmed=true. - let id = lastLoggedID() ?? "no-id-captured-yet" - do { - _ = try await ExpungeDrawerIntent(id: id, reason: "in-process test", confirmed: false).perform() - intentRunLog.insert("expunge: performed (substrate accepted confirmed=false — confirmation guard may be missing).", at: 0) - } catch { - // Expected: the substrate refuses confirmed=false. Log the - // refusal so the operator can see the guard is working. - intentRunLog.insert("expunge: substrate refused confirmed=false (confirmation guard active) — \(error).", at: 0) - } - return // Already inserted the log line above; skip the catch below. - - default: - intentRunLog.insert("\(verb): no in-process path wired.", at: 0) - } - } catch { - intentRunLog.insert("\(verb): intent threw — \(error)", at: 0) - } - } - - /// Extract the most recently captured drawer id from the run log, if any. - /// - /// The capture log line written by `runIntent("capture")` includes the - /// substrate's response first line verbatim, which is always in the form - /// `"filed memory "` (see `ToolDispatch.runFileMemory`). This method - /// scans `intentRunLog` newest-first (index 0 is newest, inserted at 0) - /// for a UUID pattern adjacent to that prefix and returns the first match. - /// Returns nil when no capture has been logged yet. - /// Internal (not private) so the `GatewayUITests` target can reach it via - /// `@testable import GatewayUI`. The function has no behavior that needs - /// external visibility — internal is the minimum required for testability. - func lastLoggedID() -> String? { - // The UUID pattern per RFC 4122: eight-four-four-four-twelve hex digits. - let uuidPattern = #"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}"# - guard let regex = try? Regex(uuidPattern) else { return nil } - for line in intentRunLog { - guard line.hasPrefix("capture:") else { continue } - if let match = line.firstMatch(of: regex) { - return String(line[match.range]) - } - } - return nil - } - - private func timestamp() -> String { - let f = DateFormatter() - f.dateFormat = "HH:mm:ss" - return f.string(from: Date()) - } - - // MARK: URL routing (A5 — x-callback-url) - - private static let log = Logger(subsystem: "com.mootx01.kit", category: "AppModel") - - /// Route an inbound x-callback-url through MootURLRouter. Called from - /// the view layer's `.onOpenURL` modifier. - /// - /// Inbound URLs are UNTRUSTED INPUT: any local process, any - /// `