> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runlayer.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Workspace ONE

> Deploy AI Watch to macOS via Workspace ONE using the signed `.pkg` and Custom Attribute–parameterized profile. Detect, endpoint modes, and Sessions ship in one package.

export const overviewTrailer_0 = <>Workspace ONE substitutes Custom Attribute lookup tokens inside uploaded profiles at deploy time. Clone the tenant profile per mode so its literal legacy <code>Enforcement</code> value stays paired with <code>Mode</code>.</>

export const adminAccess_0 = "Workspace ONE admin access for the target Organization Group."

export const tenantConfigFile_0 = "com.runlayer.aiwatch.config.ws1.mobileconfig"

export const tenantConfigPurpose_0 = <>Workspace ONE tenant-config profile (uses <code>{"{CustomAttribute1}"}</code>–<code>{"{CustomAttribute5}"}</code> lookups; pair its literal legacy <code>Enforcement</code> value before upload)</>

<Note>
  Migrating from the legacy script-based Detect rollout? Run [Legacy macOS Detect Cleanup](/shadow-ai/deploy/macos-cleanup-script-deployment) before rolling out the `.pkg` so the old `runlayer-scan` artifacts don't conflict with `com.runlayer.aiwatch`.
</Note>

## Overview

A signed, notarized `aiwatch` binary installs once per device via `.pkg`. Tenant config — host, a single **organization API key**, `Mode`, legacy `Enforcement`, the `Sessions` capability flag, and the `AutoUpdate` gate — is pushed via an MDM Configuration Profile. {overviewTrailer_0}

## Capabilities

All behaviors ship in the **same** `.pkg` and shared PPPC / Login Items profiles. The mode and flags in the tenant-config profile decide what runs — update the configuration and re-push the profile, no `.pkg` reinstall.

If you need multiple configurations, deploy one tenant-config profile per group. The `.pkg`, PPPC profile, and Login Items profile are universal and can be reused unchanged across all configurations.

Project and client-config detection is always enabled after AI Watch is installed. Endpoint behavior is selected with `Mode`; Sessions and optional runtime discovery are controlled independently. One organization API key authenticates AI Watch scans, hooks, session events, and macOS updates; no enrollment keys are required.

| Capability            | Configuration                                    | What it does                                                                                                                                                                                                     |
| --------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Detect**            | Always on                                        | Discovers AI clients, shadow MCP servers, skills, and plugins from project and client configuration files                                                                                                        |
| **Running processes** | `DetectProcesses` / `AIWATCH_DETECT_PROCESSES`   | Discovers AI tooling and configured MCP servers from running processes and listening sockets                                                                                                                     |
| **Containers**        | `DetectContainers` / `AIWATCH_DETECT_CONTAINERS` | Performs read-only MCP config, skill, and agent-definition discovery inside running Docker containers through the Docker CLI or local Engine API socket; on Linux, also scans k3s through root-only `k3s crictl` |
| **Monitor**           | `Mode=monitor` / `AIWATCH_MODE=monitor`          | Reports endpoint hook activity without applying scanner or governance decisions                                                                                                                                  |
| **Protect**           | `Mode=protect` / `AIWATCH_MODE=protect`          | Applies scanner-configured Block and Mask decisions plus fail-open, allow-unless-denied MCP source governance; explicit denylist matches are blocked                                                             |
| **Enforce**           | `Mode=enforce` / `AIWATCH_MODE=enforce`          | Applies scanner and local policy decisions to non-MCP tools plus fail-closed, deny-by-default MCP source governance; managed MCP calls scan at the gateway                                                       |
| **Sessions**          | `Sessions` / `AIWATCH_SESSIONS`                  | Installs the full event hook set for [Sessions](/platform-sessions) telemetry                                                                                                                                    |

* **Running-process and container discovery** are disabled by default. Set their macOS/Linux keys to `true` or Windows properties to `1` to opt in.
* **Monitor** is the default mode.
* **Sessions** is enabled by default. Set `Sessions=false` (macOS) or `AIWATCH_SESSIONS=0` (Windows) to skip the full event/session hook set.

`Mode` overrides the legacy `Enforcement` boolean when it contains a valid value. If `Mode` is absent or invalid, `Enforcement=true` maps to Enforce and `Enforcement=false` or absent maps to Monitor. Generated deployment artifacts still include `Enforcement` for older AI Watch versions: Monitor writes `false`, while Protect and Enforce write `true`. Mode-aware clients apply the exact selected mode; an older client receiving Protect falls back to Enforce rather than silently monitoring, but does not have Protect's exact direct-MCP scanner path.

Hook installation follows the combined capability state: AI Watch installs hook configs when Mode is Protect/Enforce or Sessions is enabled. Detect-only uses Monitor with Sessions disabled; omitting Sessions uses the default enabled state and installs hooks for Monitor telemetry.

On current macOS and Windows packages, admins can change Mode, Sessions, process/container discovery, project depth, and project timeout from **Settings → MDM configuration → Manage settings**. Devices fetch the complete settings snapshot during the privileged hook reconcile, typically within an hour, without an MDM policy update. The tenant host, organization API key, `AutoUpdate`, CPU/memory limits, and browser-extension settings remain MDM-owned.

**Advanced scan tuning (optional).** Detect scans walk each user's home directory to a bounded depth within a per-scan time budget, and a cooperative in-process governor keeps CPU and memory use within soft caps. The defaults fit most fleets — override them only if scans miss deeply-nested project configs (raise depth), run out of time on very large home directories (raise timeout), or you want to constrain the scan's footprint on constrained hosts (lower the CPU / memory caps).

| Setting               | Configuration                                | Default                | Range             |
| --------------------- | -------------------------------------------- | ---------------------- | ----------------- |
| **Project depth**     | `ProjectDepth` / `AIWATCH_PROJECT_DEPTH`     | `7`                    | `1`–`20`          |
| **Project timeout**   | `ProjectTimeout` / `AIWATCH_PROJECT_TIMEOUT` | `60`s                  | `1`–`300`s        |
| **CPU cores**         | `CpuCores` / `AIWATCH_CPU_CORES`             | half the logical cores | `1`–logical cores |
| **Max CPU percent**   | `MaxCpuPercent` / `AIWATCH_MAX_CPU_PERCENT`  | `50`                   | `5`–`100`         |
| **Memory limit (MB)** | `MemoryLimitMb` / `AIWATCH_MEMORY_LIMIT_MB`  | `1024`                 | `128`–`8192`      |

All are integers — macOS `<integer>` in the tenant-config profile, Windows `REG_DWORD` via the matching MSI `AIWATCH_*` property. Omit the keys to keep the defaults. Values outside the range are clamped into it; non-integer or non-positive values are ignored and the default applies. The CPU / memory caps are best-effort: the governor throttles the scan to hold the CPU duty cycle (`max(0.05, min(cpu_cores, MaxCpuPercent% × logical_cores))` cores) and aborts the scan if its Python-heap high-water mark exceeds the memory ceiling.

The MDM Configuration setup wizard bakes the selected **Monitor / Protect / Enforce** mode and **Collect session data** value into the downloaded profile, plus any **Advanced scan settings** overrides. When hand-editing a macOS profile, set `Mode` to `monitor`, `protect`, or `enforce`; pair legacy `Enforcement=false` with Monitor and `Enforcement=true` with Protect or Enforce; set other boolean flags explicitly and each scan-tuning key to an `<integer>`.

## Prerequisites

* {adminAccess_0}
* Devices enrolled via **UAMDM** (User-Approved MDM) or **DEP/ADE**. TCC payloads are ignored on manually-enrolled MDM.
* A single **organization API key** with the **Shadow AI Scan** role minted in **Settings → Organization API keys** in the Runlayer dashboard. Record the secret value (`rl_org_...`). The same key authenticates scanning, enforcement, sessions, and the narrow update-target/download endpoints.
* Your Runlayer tenant host URL (e.g. `https://your-instance.runlayer.com`).

<Note>
  **Apple Silicon only for now.** The current release ships an `arm64` `.pkg`.
</Note>

## Artifacts

The package is a `.zip` named `aiwatch-<version>-macos-arm64.zip`. Contents:

| File                                                                            | Purpose                                                                                                                                                                                     |
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aiwatch-<version>-macos-arm64.pkg`                                             | Signed + notarized installer (single `aiwatch` binary + scan LaunchAgent + hook bootstrap + hourly root update daemon)                                                                      |
| <code>{tenantConfigFile_0 ?? "com.runlayer.aiwatch.config.mobileconfig"}</code> | {tenantConfigPurpose_0 ?? "Tenant host + org API key + Mode / legacy Enforcement / Sessions configuration + AutoUpdate gate (edit before upload, or download pre-filled from the wizard)."} |
| `com.runlayer.aiwatch.pppc.mobileconfig`                                        | Full Disk Access / TCC grants (upload as-is)                                                                                                                                                |
| `com.runlayer.aiwatch.loginitems.mobileconfig`                                  | Pre-approves the bundled LaunchAgents on macOS 13+ (upload as-is; `BundleIdentifier=com.runlayer.aiwatch` covers all current and future user-context units)                                 |

Contact your Runlayer account team if you don't have the `.zip` yet.

The tenant profile enables `AutoUpdate` by default. Set it to `<false/>` before deployment to opt a group out; an absent key is also treated as enabled.

<Note>
  **Deploy the three Configuration Profiles before the `.pkg`.** Profiles must land in `/Library/Managed Preferences/` and TCC before the bundled LaunchAgent's first scan tick — otherwise `aiwatch` logs `host not configured` and TCC denies project-config reads until the next MDM sync.
</Note>

## Deployment

<Steps>
  <Step title="Define Custom Attributes (one-time per Organization Group)">
    1. **Devices → Provisioning → Custom Attributes → Add**.
    2. `CustomAttribute1` → set value to your tenant host URL (e.g. `https://your-instance.runlayer.com`).
    3. `CustomAttribute2` → set value to your org API key secret (`rl_org_...`).
    4. *(Optional)* `CustomAttribute3` → set value to override the device username (defaults to OS user).
    5. *(Optional)* `CustomAttribute4` → set value to override the device name (defaults to hostname).
    6. `CustomAttribute5` → set the endpoint mode: `monitor`, `protect`, or `enforce`.
  </Step>

  <Step title="Upload PPPC + Login Items profiles">
    1. **Devices → Profiles → Add → Upload**.
    2. Upload `com.runlayer.aiwatch.pppc.mobileconfig` and `com.runlayer.aiwatch.loginitems.mobileconfig` as-is. Both pre-pinned to Developer ID team `AF2M8HC7A2`.
    3. Assign to your target Smart Group.
  </Step>

  <Step title="Upload the WS1 tenant-config profile">
    1. **Devices → Profiles → Add → Upload**.
    2. Clone `com.runlayer.aiwatch.config.ws1.mobileconfig` per mode. Keep its literal `<key>Enforcement</key>` value `<false/>` for Monitor; change it to `<true/>` for Protect or Enforce. Workspace ONE cannot derive this boolean from string-valued `CustomAttribute5`.
    3. The shipped profile disables optional process and container discovery. To opt a Smart Group in, change the corresponding literal boolean to `<true/>` in its cloned profile; container discovery requires Docker.
    4. Upload the paired profile. Workspace ONE substitutes `{CustomAttribute1}`–`{CustomAttribute5}` at deploy time.
    5. Assign it to the Smart Group whose `CustomAttribute5` matches that mode.
  </Step>

  <Step title="Upload the .pkg">
    1. **Apps & Books → Internal → Add Application**.
    2. Upload `aiwatch-<version>-macos-arm64.pkg`.
    3. Assign to the same Smart Group.
  </Step>
</Steps>

After deployment, change Mode, Sessions, optional process/container discovery, project depth, or project timeout from **Settings** → **MDM configuration** → **Manage settings**; connected devices refresh them during the hourly hook reconcile. To rotate the host or API key, update the Custom Attributes and re-publish the profile. Other MDM-owned values such as `AutoUpdate`, resource limits, identity overrides, and browser-extension settings remain literal profile values. When hand-editing Mode in a profile, also deploy the copy with the matching legacy Enforcement boolean. No `.pkg` reinstall.

## Verification

On a test Mac after MDM sync:

```bash theme={null}
# 1. .pkg installed (binary + scan agent + updater)?
test -x /usr/local/bin/aiwatch && \
  test -f /Library/LaunchAgents/com.runlayer.aiwatch.plist && \
  test -f /Library/LaunchDaemons/com.runlayer.aiwatch.update.plist && \
  echo "binary + scan agent + updater OK"

# 2. Signature + notarization?
pkgutil --check-signature /path/to/aiwatch-<version>-macos-arm64.pkg
codesign -dv --verbose=4 /usr/local/lib/runlayer/aiwatch/aiwatch
# Expect: Authority=Developer ID Application: Anysource Inc. (AF2M8HC7A2)
#         Identifier=com.runlayer.aiwatch

# 3. Configuration Profile landed (host + org API key + Mode / Enforcement / Sessions)?
defaults read /Library/Managed\ Preferences/com.runlayer.aiwatch.plist

# 4. Scan agent loaded for the console user?
launchctl print "gui/$(id -u)/com.runlayer.aiwatch"

# 5. Hourly update daemon loaded in the system domain?
sudo launchctl print system/com.runlayer.aiwatch.update

# 6. Login Items profile applied (macOS 13+)?
sfltool dumpbtm | grep -i runlayer

# 7. PPPC grants visible to TCC?
sudo log show --predicate 'subsystem == "com.apple.TCC"' --last 5m | grep -i aiwatch

# 8. Trigger an immediate scan
/usr/local/bin/aiwatch scan

# 9. (Protect, Enforce, or Sessions) Confirm hooks are installed for the console user
/usr/local/bin/aiwatch setup hooks check --mdm
# Expect: exit 0 (compliant). Exit 1 = hook configs drifted (the next bootstrap tick rewrites them).

# 10. View process activity; update failures also appear as a non-zero
# last exit code in the launchctl output from step 5.
log show --predicate 'process == "aiwatch"' --last 1h
# Tail in real time:
log stream --predicate 'process == "aiwatch"'
```

In the Runlayer dashboard, navigate to **Shadow** and confirm scan data is arriving from the test device.

## Common post-deploy issues

<AccordionGroup>
  <Accordion title="Users see 'Background Item Added' after install">
    Confirm `com.runlayer.aiwatch.loginitems.mobileconfig` is scoped to the same device group as the `.pkg`, then force an MDM sync and check:

    ```bash theme={null}
    sfltool dumpbtm | grep -i runlayer
    ```

    On macOS 13+, expect the Runlayer item to show `enabled allowed visible`. macOS 12 and older ignore the Login Items payload; the notification control only exists on macOS 13+.
  </Accordion>

  <Accordion title="Gatekeeper blocks the package">
    Verify you are deploying the official signed and notarized release artifact:

    ```bash theme={null}
    pkgutil --check-signature /path/to/aiwatch-<version>-macos-arm64.pkg
    spctl --assess --type install -vv /path/to/aiwatch-<version>-macos-arm64.pkg
    ```

    Expected: a Developer ID Installer signature for Anysource Inc. and `source=Notarized Developer ID`.
  </Accordion>

  <Accordion title="PPPC applied but TCC still denies access">
    The PPPC profile pins Full Disk Access to identifier `com.runlayer.aiwatch` and Developer ID team `AF2M8HC7A2`. Confirm the installed binary matches:

    ```bash theme={null}
    codesign -dv --verbose=4 /usr/local/lib/runlayer/aiwatch/aiwatch
    ```

    Expected: `Authority=Developer ID Application: Anysource Inc. (AF2M8HC7A2)` and `Identifier=com.runlayer.aiwatch`. Custom or ad-hoc signed builds will not satisfy the PPPC CodeRequirement.
  </Accordion>
</AccordionGroup>

## Customize scan schedule (optional)

The bundled LaunchAgent ships with a 15-minute `StartInterval`. Most tenants don't need to change it. To override, push the script below via your MDM's recurring-script mechanism (Jamf Policy, Kandji Custom Script, SimpleMDM Script, etc.):

```bash theme={null}
#!/bin/bash
set -euo pipefail
PLIST=/Library/LaunchAgents/com.runlayer.aiwatch.plist
INTERVAL=1800   # 30 min — edit per tenant
/usr/libexec/PlistBuddy -c "Set :StartInterval ${INTERVAL}" "$PLIST"

CONSOLE_UID=$(stat -f %u /dev/console 2>/dev/null || echo "")
if [ -n "$CONSOLE_UID" ] && [ "$CONSOLE_UID" != "0" ]; then
    launchctl bootout "gui/${CONSOLE_UID}/com.runlayer.aiwatch" 2>/dev/null || true
    launchctl bootstrap "gui/${CONSOLE_UID}" "$PLIST" 2>/dev/null || true
fi
```

<Note>
  The next `.pkg` upgrade resets `StartInterval` to the bundled default — the override script must run on a recurring schedule, or be re-applied after each upgrade.
</Note>

## Upgrade

By default, the root `com.runlayer.aiwatch.update` LaunchDaemon checks hourly and installs the backend-selected signed + notarized `.pkg`. An MDM Custom App push remains a manual fallback, but the next hourly tick will restore the backend-selected target. To hold a manually pushed version, set `AutoUpdate=false` or align the backend rollout policy first.

The `pkgbuild` receipt (`com.runlayer.aiwatch`) tracks every file in the install layout; `installer` removes any files in the old receipt not present in the new payload and atomically lays down the new tree per file. Mid-scan upgrades are safe — macOS lets you unlink an open file, the running `aiwatch` keeps its mapped `.dylib` pages until the next LaunchAgent tick picks up the new version. Postinstall intentionally leaves an already-loaded updater running so it cannot terminate its own installer transaction; the hourly cadence remains stable.

The three Configuration Profiles (tenant config, PPPC, Login Items) are unchanged across version upgrades.

<Warning>
  If you've overridden `StartInterval` via Custom Script, the upgrade resets it to the bundled default. Re-apply the override script on a recurring schedule.
</Warning>

## Uninstall

For full package and hook cleanup guidance, see [Remove AI Watch](/shadow-ai/deploy/remove).

## Troubleshooting

For deployment diagnostics and common macOS package issues, see [Troubleshooting](/shadow-ai/troubleshooting).
