vibe coded with ❀️

Documentation

How entAI routes requests, and how to configure it by MDM.

Concept app. A working exploration of an idea, built and shared to show what's possible β€” not a supported commercial product. Expect rough edges, no SLA and no roadmap guarantees. It has been tested in a demo environment only, not in production. Try it on test Macs first; feedback is very welcome.

Overview

entAI brings your organization's OpenAI-compatible AI service to the Mac. Every request runs on the on-device model first; only what it can't handle goes to the company AI, and only as your MDM policy allows. Two surfaces: a menu bar chat (window + a Spotlight-style Quick Chat opened by double-pressing βŒ₯) and right-click Services actions on selected text and files.

No telemetry. entAI talks to nothing but the company AI endpoint you configure (and Entra ID for sign-in, if used). Chat history stays on the Mac unless you disable retention.

Requirements

  • macOS 26 or later, Apple Silicon only
  • 8 GB RAM minimum β€” the bundled on-device model needs about 2.7 GB free
  • An OpenAI-compatible endpoint for cloud escalation (optional β€” entAI works local-only without one)

Installation & Deployment

  1. Install EntAI-1.0.0.pkg β€” signed and notarized, installs /Applications/EntAI.app (~2.6 GB, including the on-device model).
  2. Configure with a profile for domain com.spectrechen.entai, starting from the example profile below. Without a profile, entAI works device-only, and the user can still enter a server themselves in Settings.
  3. (Recommended) grant Accessibility via PPPC β€” see PPPC & Accessibility.
  4. Company AI credentials: Entra ID needs no secret; API keys go in the user's keychain β€” see API Keys.
  5. (Optional) add Scripts/ea-entai-usage.sh as a Jamf Extension Attribute or Intune custom attribute β€” see Usage Statistics.

entAI starts at login with a menu bar icon and no Dock icon. It ships without keyboard shortcuts for its Services entries, so it never overrides shortcuts you already use β€” users assign their own under System Settings β†’ Keyboard β†’ Keyboard Shortcuts β†’ Services.

How Configuration Works

Every setting is optional. A key set by a configuration profile is locked in Settings (shown with a lock badge); any key you don't set stays user-editable. Booleans accept either <true/> or the string "true". Changes are picked up the next time entAI becomes active β€” macOS doesn't notify running apps when a new profile lands.

Managed Preferences are plaintext. Never put API keys or secrets directly in a configuration profile. Cloud API keys belong in the keychain β€” see API Keys β€” and Entra ID needs no secret at all.

How Routing Works

SituationGoes to
Fits the local model (≀ LocalMaxInputTokens, default 6,000 β€” roughly 4–5 pages)This Mac
Too long, contains images, or no local model fits in free memory right nowCompany AI β€” asked first under CloudPolicy=ask, automatically under automatic, refused under never
User explicitly chose "Keep on this Mac" / "Ask Company AI" (per message, or Try Again)That choice β€” an explicit cloud choice always counts as consent
Action with ActionRouting = local / cloudAlways local, or preferably cloud
Neither is possibleRefused, with the reason and what to do β€” never chunked or truncated

The local model is chosen per request from the most capable installed model that fits into free memory at that moment β€” the model itself never decides where it runs, the router does, deterministically. On 8 GB Macs under memory pressure, expect more frequent cloud escalation. The local model loads on first use and unloads after 5 idle minutes.

Company AI (Cloud) Keys

KeyTypeDefaultMeaning
CloudBaseURLStringemptyOpenAI-compatible endpoint, e.g. https://ai-gateway.corp.example/v1. An Azure resource URL gets /openai/v1 appended automatically. Must be HTTPS (localhost excepted). Empty = no cloud at all.
CloudAuthModeStringapiKeyapiKey / azureAPIKey / entraID / none
CloudDefaultModelStringβ€”Model name, or the Azure deployment name
CloudModelsArray of stringsβ€”Models the user may pick from; the default model is always included
CloudModelPickerEnabledBooltrueShow the model picker (only relevant if CloudModels has more than one)
CloudDisplayNameStringCompany AIName shown in the UI, e.g. "Send to Contoso AI?"
CloudAllowInsecureHTTPBoolfalseTest labs only β€” accept http:// endpoints other than localhost
CloudPolicyStringaskSee below
CloudCostPer1KTokensReal0Blended cloud price, shown as "cloud cost avoided" in Settings β†’ Usage only

CloudPolicy β€” exact semantics

  • never β€” nothing leaves the Mac; requests the local model can't handle are refused.
  • ask β€” the user confirms each automatic escalation. An explicit "Ask Company AI" click counts as consent.
  • automatic β€” escalates without asking; every answer still shows its source badge.

On-Device Model

KeyTypeDefaultMeaning
LocalModelEnabledBooltruefalse = cloud only
LocalMaxInputTokensInteger6000Estimated-token ceiling (including history/files) for the local model; above this it escalates or refuses
UserRouteOverrideEnabledBooltrueWhether users may choose "Keep on this Mac" / "Ask Company AI" per message and retry
ActionRoutingDictβ€”Per-action override, e.g. summarize β†’ cloud

Larger models can be deployed to /Library/Application Support/entAI/Models/<model-id>. The bundled model is Gemma 4 E2B (text-only, Apache 2.0 licensed).

Chat Settings

KeyTypeDefaultMeaning
ChatHistoryEnabledBooltrueKeep conversations on disk
ChatHistoryRetentionDaysInteger30Delete untouched conversations after N days; 0 = keep forever
AttachmentsEnabledBooltrueFiles in chat and Finder file actions
ShowRouteBadgeBooltrueShow the "On this Mac Β· model" / "Company AI Β· model" badge
HotkeyEnabledBooltrueDouble-press βŒ₯ opens Quick Chat (needs Accessibility)
LaunchAtLoginBooltrueRegister as a login item
entAI Settings, Privacy and Chat tab, showing the Send to company AI radio buttons and Chat history controls
Settings β†’ Privacy & Chat β€” the same CloudPolicy choice and chat history controls, in the UI.

Actions & Custom Actions

Built-in actions, in Services-menu order: summarize, translate, rephrase, improve, fixGrammar, rewriteFormal, rewriteCasual, rewriteConcise, explain, keyPoints, actionItems, draftReply.

KeyTypeMeaning
EnabledActionsArrayWhich built-in actions are available (default: all)
CustomActionsArray of dictsFields: ID, Title, Prompt (required), Icon (SF Symbol), Routing (auto/local/cloud), Input (text/file/both), OutputRatio (default 1.0). A custom action reusing a built-in ID replaces it.
TranslationLanguagesArrayLanguages offered for Translate (default: English, German, French, Spanish, …)
DefaultTranslationLanguageStringTranslate target language β€” defaults to the Mac's own language

The Services menu itself is fixed (Summarize, Translate, Rephrase, Fix Spelling & Grammar, Explain, More Actions…, Ask in Chat; for files: Summarize File, Ask About File, File Actions…) β€” everything beyond those lives under "More Actions…" / "File Actions…". Disabling a fixed entry still shows it, but tells the user it's disabled rather than just disappearing silently.

Microsoft Entra ID β€” Setup Steps

Not yet verified against a real tenant. Entra ID sign-in is implemented and unit-tested (PKCE, URLs, token parsing) but has not been exercised against a live Microsoft Entra tenant. Verify in a test tenant before rolling it out.

entAI authenticates as a public client β€” OAuth 2.0 authorization code + PKCE via ASWebAuthenticationSession, no client secret, no MSAL. After the first sign-in, tokens refresh silently from the keychain; with the Microsoft Enterprise SSO plug-in and entAI allow-listed, the sign-in sheet completes without a password at all.

  1. In the Entra admin center: App registrations β†’ New registration β€” name it entAI, single tenant.
  2. Authentication β†’ Add a platform β†’ Mobile and desktop applications β€” custom redirect URI msauth.com.spectrechen.entai://auth.
  3. Authentication β†’ Advanced settings β€” set Allow public client flows: Yes.
  4. API permissions β€” for Azure OpenAI directly, add Azure Cognitive Services β†’ user_impersonation and grant admin consent. For your own gateway (e.g. APIM), add the gateway API's own scope instead and set EntraScope accordingly.
  5. Note the Application (client) ID and Directory (tenant) ID for the profile below.

For Azure OpenAI, the signed-in user or group also needs the Cognitive Services OpenAI User role on the Azure OpenAI resource itself.

Profile keys

<key>CloudAuthMode</key><string>entraID</string>
<key>EntraTenantID</key><string>contoso.onmicrosoft.com</string>
<key>EntraClientID</key><string>...client id...</string>
<!-- only when routing through a gateway rather than Azure OpenAI directly: -->
<key>EntraScope</key><string>api://.../.default</string>
KeyDefaultMeaning
EntraTenantIDβ€”Tenant ID or domain
EntraClientIDβ€”Application (client) ID of the public-client app registration
EntraScopehttps://cognitiveservices.azure.com/.defaultToken scope β€” override for a gateway's own App ID URI + /.default
EntraRedirectURImsauth.com.spectrechen.entai://authMust match the app registration exactly

For silent sign-in via the Microsoft Enterprise SSO plug-in, add entAI to its allow list in the Extensible Single Sign-On payload:

<key>AppPrefixAllowList</key>
<string>com.microsoft.,com.apple.,com.spectrechen.entai</string>

Entra Error Codes

ErrorCause
AADSTS50011Redirect URI mismatch β€” recheck the platform configuration step
AADSTS7000218Public client flows not allowed β€” enable that in Authentication β†’ Advanced settings
401 from Azure OpenAIMissing the Cognitive Services OpenAI User role, or wrong EntraScope

Errors surface with their exact Entra code in both Settings β†’ Company AI and directly in chat. Signing out from Settings β†’ Company AI removes the stored refresh token.

Data & Privacy

WhatWhereLeaves the Mac?
Chat history~/Library/Application Support/entAI/Conversations/*.json (owner-only)No β€” deleted after ChatHistoryRetentionDays, or disable entirely with ChatHistoryEnabled
Usage counts (no content)~/Library/Application Support/entAI/UsageStats.plistOnly if your inventory script collects it
API key / Entra refresh tokenLogin keychain, service com.spectrechen.entaiSent only to your configured service
Prompts and filesMemory onlyOnly when routed to the company AI, as policy allows β€” always with a visible source badge

PPPC & Accessibility

Recommended: allow Accessibility for com.spectrechen.entai, needed for double-press βŒ₯ to work in other apps and for Replace to work in text actions. Without it, βŒ₯βŒ₯ only works inside entAI itself, and Replace falls back to just copying.

anchor apple generic and identifier "com.spectrechen.entai" and certificate leaf[subject.OU] = "23VFRTF5KC"

API Keys

API keys are never stored in configuration profiles β€” they live in the user's login keychain, service com.spectrechen.entai, account apiKey. A user can paste one into Settings directly, or a script can provision it (run as the user):

security add-generic-password -U -s com.spectrechen.entai -a apiKey \
  -T "/Applications/EntAI.app" -w "$KEY"

Example Profile

The distribution's example profile (Azure OpenAI + Entra ID) demonstrates most of the config surface in one file:

  • Company AI: CloudBaseURL, CloudAuthMode=entraID, CloudDefaultModel, CloudModels, CloudDisplayName, CloudPolicy=ask, CloudCostPer1KTokens, plus the matching EntraTenantID/EntraClientID.
  • Routing: LocalMaxInputTokens=6000, an ActionRouting entry forcing fixGrammar local.
  • Chat: history enabled, 30-day retention, attachments on.
  • Custom actions: a "Customer Reply" action tuned to a house tone, and a "Check Contract Wording" action forced to the cloud.

Usage Statistics

entAI Settings, Usage tab, showing counts answered on this Mac, answered by company AI, and tokens used
Settings β†’ Usage β€” the same counts the Jamf EA / Intune script reports, right in the app.

Scripts/ea-entai-usage.sh reports, for the console user, from ~/Library/Application Support/entAI/UsageStats.plist β€” counts only, never content:

<result>local=120 cloud=30 refused=2 localShare=80% localTokens=412000 cloudTokens=98000 since=2026-09-28</result>

Disable collection entirely with UsageStatsEnabled = false.

Troubleshooting

SymptomCause / fix
No entAI entries under ServicesLog out and back in once after install, or run /System/Library/CoreServices/pbs -update; check System Settings β†’ Keyboard β†’ Keyboard Shortcuts β†’ Services
βŒ₯βŒ₯ only works inside entAI; Replace only copiesAccessibility isn't allowed yet β€” grant it via PPPC or System Settings β†’ Privacy & Security β†’ Accessibility
"On-device model not available: not enough free memory"Free memory is below what the model needs right now; with a company AI configured, the request routes there instead
"The company AI is not available (…)"The bracketed reason names what's missing β€” address, model, API key, or sign-in; check with Settings β†’ Company AI β†’ Test Connection
HTTP 401 / 403Wrong key, an expired Entra sign-in, or a missing role on the Azure OpenAI resource
HTTP 404Wrong model/deployment name or endpoint address
Long pause, then "Thinking…"The cloud model is a reasoning model and streams its reasoning before any visible answer β€” pick a non-reasoning model for speed

Logs: log show --predicate 'subsystem == "com.spectrechen.entai"' --last 1h --info β€” routing decisions with token estimates, never content.

Uninstall: sudo Scripts/uninstall.sh (add --purge to also remove each user's history, stats and keychain items).