# AgentDeck Dashboard — App Review Notes

_Paste the relevant sections into App Store Connect's "Notes" field when submitting `apple-v<version>`._

## What AgentDeck does

AgentDeck Dashboard is a real-time monitoring and evaluation app for AI coding agents (Claude Code, opt-in Codex lifecycle hooks, opt-in OpenCode local-server monitoring, and OpenClaw Gateway sessions). The submitted macOS app's built-in Swift daemon owns the dashboard server, hook ingestion, session state, local-network pairing, APME Layer 2 evaluation, and native hardware modules. It shows live status, tool activity, and quality scores on Mac and on the paired iOS companion. When an agent genuinely waits for a decision, AgentDeck posts a local display-only notification; the user answers in their own agent interface.

**Works standalone on Mac.** The review scope is a clean Mac running only the submitted Swift app. Dashboard, APME reports, the 17-target Device Preview, opt-in Claude/Codex observation, opt-in OpenCode/OpenClaw monitoring, and iOS pairing work without another AgentDeck executable. Users start their chosen agent independently and AgentDeck receives only the integrations they enable.

**Optional hardware extensions** render the same state on Stream Deck+ (Elgato plugin), Ulanzi D200H (Ulanzi Studio plugin), supported ESP32 status displays (native serial/network), Divoom Pixoo (network), and iDotMatrix / Divoom Timebox Mini (native CoreBluetooth). Hardware is not required.

The product page and screenshots describe only this standalone Swift-daemon experience. Features that are not implemented by the submitted Swift app are not claimed and are not part of the review flow.

The app is sandboxed. Entitlements below support local monitoring and user-selected integrations. There is no analytics or advertising SDK. The optional Anthropic API evaluation backend is off by default and is disclosed in App Privacy; the default Foundation Models backend stays on-device.

## Network server rationale (port 9120+)

`com.apple.security.network.server` is used to run a local-only HTTP + WebSocket dashboard hub on `127.0.0.1:9120`. The reason this must be a server (and not an outbound client) is that AgentDeck is a companion to **the same user's own iPad/iPhone running the AgentDeck Dashboard iOS app**, which connects over the local Wi-Fi network and renders the same data. Without `network.server`, the iOS companion cannot receive live session events.

- Port 9120 is the default; the user can override via Settings when another process holds it.
- Binding is `127.0.0.1` + the local-network interface. We do not open firewall rules or bind externally. No inbound traffic from the public internet.
- The server exposes only read-only dashboard endpoints + a hook POST endpoint that the Claude Code CLI (running in the user's own terminal) uses to report session events.

## Bonjour (`_agentdeck._tcp`)

Used so the iOS companion can discover the Mac dashboard on the same LAN without asking the user for an IP address. `NSLocalNetworkUsageDescription` in Info.plist explains this to the user at the system prompt.

## Sandbox Data Container

AgentDeck stores daemon state (session registry, auth token, cached usage metrics, APME evaluation SQLite database) inside the app's own sandbox container at `Application Support/AgentDeck`. The build does not request the optional App Groups entitlement because the submitted app has no helper, extension, or login item. Data stays local unless the user explicitly selects a disclosed network integration such as Anthropic API evaluation.

## Hook installation (Claude Code settings file)

AgentDeck can optionally register hooks in `~/.claude/settings.json` so Claude Code sessions report state to the dashboard. This is entirely opt-in:

1. On first launch the dashboard does **not** touch that file.
2. The user navigates to Settings → Claude Code Hooks → "Enable Claude Code Hooks…".
3. An `NSAlert` explains what keys will be written.
4. On acceptance, an `NSOpenPanel` requires the user to explicitly pick `~/.claude/settings.json`. Only then do we acquire a security-scoped bookmark and write the hook entries.
5. Writes are scoped to this single user-selected file; the app reads no other files in `~/.claude/`.

The UI also offers a "Remove" button that deletes our hook entries and revokes the bookmark.

## Local notifications

AgentDeck posts a local `UNUserNotification` when a monitored session genuinely waits for the user's response (e.g. Claude Code shows a permission prompt in the user's terminal), and clears it the moment the session moves on. Authorization is requested through an explanatory in-app prompt on first launch (the user can decline; a "Request Again" affordance lives in Settings). No push notifications, no remote notification service — everything is local.

## USB HID entitlement (`com.apple.security.device.usb`)

Used to communicate with the optional Ulanzi D200H Deck Dock (USB HID class, VID `0x2207` / PID `0x0019`). The user opts in by plugging in their own hardware. If the device is absent, the feature is inert.

## Bluetooth entitlement (`com.apple.security.device.bluetooth`)

Used to communicate with the optional iDotMatrix and Divoom Timebox Mini LED pixel displays over Bluetooth Low Energy, using Apple's first-party CoreBluetooth framework (no subprocess, no bundled interpreter). AgentDeck acts only as a BLE *central*: it scans for the user's own display (iDotMatrix advertised name prefix `IDM-`; Timebox Mini advertised name `TimeBox-mini-light`), connects, and writes display frames over a GATT characteristic. The user opts in by pairing their own hardware from an in-app Settings sheet; the `NSBluetoothAlwaysUsageDescription` string explains the purpose at the system prompt. If no such display is present (or the user never pairs one), the feature is inert.

## Audio input + serial entitlements

- Microphone (`com.apple.security.device.audio-input`) drives the optional voice-command input for AI sessions. Usage description in Info.plist explains the purpose at the system prompt.
- Serial (`com.apple.security.device.serial`) is only used by optional ESP32-based status displays over USB-serial. Inert unless the user connects such hardware.

## Subprocess execution

**The App Store build of AgentDeck does not spawn any subprocess or create shell scripts for Terminal.** The macOS source tree contains no `Process()` invocation, no `.command` script writer, no AppleScript paths, and no probes for external binaries (`security`, `sqlite3`, `bin/sh`, `/usr/bin/env`, `openclaw`, `whisper-cli`, `networksetup`, `node`, `adb`). The `AGENTDECK_APP_STORE` Swift compile condition is retained as a defense-in-depth gate, and a CI script (`apple/scripts/verify-appstore-archive.sh`) runs after archive to fail the pipeline if any forbidden path string ever reappears in the shipped `.app`'s main Mach-O, or if any bundled executable besides the signed AgentDeck binary is present.

### What about the Claude Code hook commands?

Claude Code hooks run `python3` / `curl` at the user's shell prompt, in their own terminal session, under Claude Code's process tree — not AgentDeck's. The hook *string* is data AgentDeck writes (with the user's explicit consent via `NSOpenPanel` + security-scoped bookmark) into `~/.claude/settings.json`. Claude Code's own runtime is what eventually executes that string when the user runs Claude Code. AgentDeck itself only receives HTTP POSTs from those hooks on `localhost:9120`.

### Bundled helpers

The App Store archive contains no `Contents/Helpers/`, no `Contents/Resources/node`, no `Contents/Resources/agentdeck-runtime`, and no `Contents/Resources/bridge/cli.js`. The sole binary is `Contents/MacOS/AgentDeck`; every feature claimed on the product page is implemented by that Swift app and its sandbox-approved frameworks.

### OpenClaw Gateway integration

Unlike the other advanced integrations, OpenClaw **is** first-class in the App Store build — entirely through the local network and (optionally) explicit user-selected file scope, never through subprocess or unsolicited file I/O:

- AgentDeck connects to the user's local OpenClaw Gateway over WebSocket (`ws://127.0.0.1:18789`). The Gateway itself is the user's own program, started independently. AgentDeck never spawns the `openclaw` CLI and never enumerates the contents of `~/.openclaw/`. The only programmatic touch of that path is a single `FileManager.fileExists` existence check used to decide where the Import-token open-panel should *start* navigation (see step 2): if `~/.openclaw/` exists, the panel's `directoryURL` is set there so the (non-hidden) `openclaw.json` is immediately visible; otherwise it falls back to the user's real home directory. `directoryURL` is only a Powerbox navigation hint and grants no read access by itself. No file under that directory is read unless the user explicitly selects it in step 2.
- On first launch AgentDeck generates its own Ed25519 keypair (stored in the macOS Keychain, accessible-after-first-unlock / this-device-only). The public key's SHA-256 hex becomes the `deviceId` sent to the Gateway during the v3 pairing handshake. A short-lived `deviceToken` issued by the Gateway is used for subsequent reconnects.
- The user must approve the new device in OpenClaw's Web UI — AgentDeck only displays the pairing state; it never writes to OpenClaw's own config.
- **Optional shared-token mode.** If the user's Gateway is configured to require a shared token in addition to (or instead of) device pairing, the user can provide it in two ways:
  1. Paste the value into Settings → Integrations → OpenClaw → Advanced ("Shared Gateway token") and tap "Save".
  2. Tap "Import token" in the OpenClaw troubleshoot row. AgentDeck presents an `NSOpenPanel`. The panel's title and message text mention `~/.openclaw/openclaw.json` as the *typical* OpenClaw config location — this is human-readable hint text only. `directoryURL` is set to `~/.openclaw/` when that folder exists (so the non-hidden `openclaw.json` is immediately visible), otherwise to the user's real home directory (resolved via `getpwuid(getuid()).pw_dir`, since `NSHomeDirectory()` inside the sandbox returns the app's container path which is not where OpenClaw lives). The only programmatic file-system call before the user selects anything is a single `FileManager.fileExists` check to choose that starting folder — no directory enumeration. The panel itself runs in Powerbox outside the sandbox and is allowed to present that path as a navigation starting point; the hint grants no read permission by itself — only what the user explicitly selects in the panel is readable. The panel does **not** preselect any specific file, the user is free to navigate elsewhere or cancel, and AgentDeck never opens, traverses, or enumerates any path under the user's real home outside the file the user picks here. The selected file's bytes are read into memory (`Data(contentsOf:)`) and parsed via `JSONSerialization`; from the resulting object **only the gateway token string is used** — it is written to the macOS Keychain via `OpenClawGatewayTokenStore`, and every other parsed value goes out of scope and is freed when the import method returns. The read is performed inside a `startAccessingSecurityScopedResource()` / `defer stop` pair under the existing `com.apple.security.files.user-selected.read-write` entitlement. AgentDeck additionally stores an app-scoped security-scoped bookmark to **the single file the user picked** (under `com.apple.security.files.bookmarks.app-scope`, the same mechanism used for the Claude/Codex/Antigravity file integrations) so that a later rotated token can be re-read from that same file on reconnect — never any other path. The bookmark is revoked when the user taps "Clear" on the token.
- Reviewers without OpenClaw installed will see "Not configured" and can skip this integration entirely. The "Import token" button is also fully optional — reviewers can validate the panel without selecting any file.

### OpenCode (opt-in local server monitoring)

OpenCode session monitoring in the App Store build is an **opt-in, read-only local network client** to a server the user runs themselves — the same posture as the OpenClaw Gateway integration:

- The feature is **off by default**. While off, AgentDeck makes zero OpenCode-related network probes. The user enables it from Settings → Integrations → OpenCode ("Monitor OpenCode server").
- When enabled, AgentDeck connects (plain `URLSession`, `com.apple.security.network.client`) to an OpenCode HTTP server that the user started independently in their own terminal (`opencode serve`). Discovery is limited to exactly three inputs: (1) the URL the user typed into Settings (default `http://127.0.0.1:4096`), (2) a health check of OpenCode's fixed default port 4096 (the same shape as the OpenClaw fixed-port probe on 18789), and (3) macOS process metadata (`sysctl KERN_PROC_ALL` / `KERN_PROCARGS2` — the same first-party API already used for Codex Desktop detection) to notice an `opencode` process the user launched with an explicit `--port N` argument. AgentDeck does **not** port-scan, does not install OpenCode plugins, and never spawns `opencode`.
- The connection consumes OpenCode's own read-only event stream (`GET /global/event`, Server-Sent Events) and renders session rows (project, working/idle, current tool). When OpenCode reports a permission request, AgentDeck shows a display-only "needs attention" state — the user responds in their own OpenCode terminal; AgentDeck sends no commands to the server.
- Reviewers without OpenCode installed see "Not configured" with the toggle off; the integration is fully inert. No OpenCode-related copy in the App Store app prompts the user to install or launch anything.

### Antigravity (usage only in the App Store build)

Antigravity coding-session monitoring is intentionally **not** claimed by the submitted app. Its only App Store feature is a user-approved local status integration: the user explicitly selects the Antigravity `state.vscdb` file through an `NSOpenPanel`, AgentDeck stores a security-scoped bookmark to that one file, and the dashboard displays plan/credit information only when parseable. AgentDeck does not install Antigravity hooks, spawn another app, enumerate the user's real home directory, or scan processes.

### Codex observation path

Codex (OpenAI's CLI) is observed through a fully opt-in path that mirrors the Claude Code hook flow:

1. The user enables it from Settings → Integrations → "Enable Codex Observation…".
2. An `NSAlert` explains that AgentDeck will write Codex lifecycle hook entries into `~/.codex/config.toml`, with optional `notify` and `[otel]` fallback entries when the user has not already configured those channels.
3. On acceptance, an `NSOpenPanel` requires the user to explicitly pick `~/.codex/config.toml`. Only then do we acquire a security-scoped bookmark and write the entries.
4. AgentDeck only edits inside its own fenced TOML block (`# >>> AgentDeck managed (do not edit) <<<` / `# <<<` markers) — user keys (`model`, profiles, MCP servers) are preserved verbatim. If the user already wrote `[features]` or `[hooks]`, AgentDeck aborts instead of unsafe-merging lifecycle hooks. If the user already wrote a top-level `notify` or `[otel]`, AgentDeck still installs lifecycle hooks but omits that optional fallback/exporter to avoid duplicate keys.
5. The values written enable `[features] hooks = true` and inline `[[hooks.*]]` command hooks. Those commands resolve AgentDeck's local-only HTTP port and POST Codex hook JSON to `http://127.0.0.1:$PORT/hooks/codex_session_start`, `/hooks/codex_user_prompt_submit`, `/hooks/codex_tool_start`, `/hooks/codex_tool_end`, and `/hooks/codex_stop`. Optional fallback entries POST notify JSON to `/hooks/codex_turn_complete` and OTel JSON traces to `/otel/v1/traces` (`protocol = "json"`). All endpoints listen on `127.0.0.1` only. The OTel `endpoint` is rewritten on every daemon startup so it always reflects the actual port the daemon is bound to.
6. At runtime, Codex (the user's separately-installed CLI) is what eventually executes that snippet under its own process tree — not AgentDeck. AgentDeck itself only receives HTTP POSTs.

The Settings panel offers a "Remove" button that strips AgentDeck's fenced block and revokes the bookmark; it leaves all user-authored TOML keys untouched. Reviewers without Codex installed see "Not configured"; the panel remains inert.

Separately, the Codex Integrations row offers an optional **usage display** that mirrors the Antigravity `state.vscdb` pattern exactly: the user explicitly picks their `~/.codex` folder through an `NSOpenPanel` (`canChooseDirectories`, starting at `~/.codex` resolved via `getpwuid(getuid()).pw_dir`, no preselection, no enumeration before selection), AgentDeck stores an app-scoped security-scoped bookmark to that one folder, and reads are performed inside a `startAccessingSecurityScopedResource()` / `defer stop` pair under the existing `com.apple.security.files.user-selected.read-write` entitlement. From that folder AgentDeck reads only `auth.json` (ChatGPT plan + subscription expiry) and the trailing bytes of the newest `sessions/.../rollout-*.jsonl` file (the `rate_limits` snapshot Codex itself writes — a 5h/7d-style usage gauge). No OpenAI/ChatGPT network endpoint is contacted for this; it is the user's own local files, parsed with `JSONSerialization`. There is no home-relative-path entitlement and no subprocess; when the user has not granted the folder the row simply shows the plan/usage region collapsed. A "Remove access" button revokes the bookmark.

AgentDeck observes Codex and OpenCode only through the opt-in Swift paths described above; users start their chosen agent independently.

## APME evaluation module

AgentDeck can evaluate finished agent turns against configurable rubrics. In the App Store build:
- Default backend is **Apple Intelligence (Foundation Models)** — on-device, zero-cost, no network.
- Alternatives (MLX local server, Anthropic API) are opt-in and clearly labeled in Settings.
- Layer 1 deterministic checks (git/pnpm introspection) are **disabled** in the App Store build because they require subprocess access outside the sandbox. The UI surfaces this explicitly — users still get Layer 2 LLM-based scoring.

## Timeline summary backend

The dashboard timeline shows a one-line topic under each completed turn (the `chat_end` row). To produce that label, the App Store build calls a chain of cost-free local backends:
- **Apple Intelligence (FoundationModels)** — on-device, zero-cost, no network. Used first when available (macOS 26+, Apple Silicon, Apple Intelligence enabled). Same framework call shape as APME.
- **MLX local server** — outbound HTTP to `127.0.0.1:8800`, only attempted when the user has independently started a local MLX server. AgentDeck does not install, prompt for, or spawn MLX; if the server isn't running, the call returns nil within ~2s and the chain falls through.
- **Heuristic** — pure-Swift first-line extractor. Always available; runs as the chain's floor.

No subprocess, no bundled interpreter, no third-party network call, no install nudge. Users can pick a specific backend (or stick with the default `Auto`) from Settings → Timeline summary. The same `verify-appstore-archive.sh` CI gate that covers APME also covers this path because both share `TimelineSummarizer.swift` / `ApmeJudgeFoundationModels.swift` and only use entitlements already declared (`com.apple.security.network.client` for outbound localhost).

## Stream Deck+ dependency

AgentDeck's Stream Deck+ integration renders session state on Stream Deck+ keys via Elgato's Stream Deck plugin SDK. This optional hardware path requires Elgato's Stream Deck software. If a user plugs in a Stream Deck+ without Elgato's software, AgentDeck shows that the integration is unavailable and links to Elgato's setup information. Reviewers testing without Elgato hardware can skip this integration entirely — the rest of the app does not depend on it.

## iOS companion

The iOS app (same bundle family `bound.serendipity.agent.deck`) is a read-only remote dashboard that auto-discovers a paired Mac via Bonjour. On first launch it runs a 3-pane onboarding walking the user through installing an agent on their Mac and finding their Mac on Wi-Fi. Fallback pairing via QR code (Mac shows → iPad scans) handles cases where Local Network permission is denied or the two devices are on different routable networks. No network-server entitlement is needed on iOS — it's a pure client.

## Review demo account

No account required. To see the app's features:

1. Launch the app. A first-run onboarding sheet walks the user through the value prop, available AI agents, and iPad pairing. Dismissing it opens the empty dashboard with a prompt to "Preview Devices".
2. Click "Preview Devices" from the menu bar to see how AgentDeck renders sessions on 17 built-in preview targets — no real hardware required.
3. After hooks are enabled, sessions the user starts independently appear automatically in the dashboard. AgentDeck never launches Terminal scripts or command-line tools itself.
4. Click "Pair iPad" to show a QR code the iOS companion app can scan.
5. Open Settings → Hardware Setup to see the in-app flows for ESP32 and Pixoo provisioning (no subprocess calls; writes serial config directly).

## Standalone review scope

Review on a clean Mac with only AgentDeck installed. The app starts its own Swift dashboard server and exposes the complete App Store experience described above. No external AgentDeck process, developer bridge, or terminal setup is part of the review instructions. The CI script `apple/scripts/verify-appstore-archive.sh` fails the build if the shipped app contains a subprocess spawn path, bundled helper executable, or home-relative-path entitlement.

## Contact

For anything unclear: `puritysb@gmail.com`.
