The policy file
Asylum reads /Library/Application Support/asylum/policy.json at launch and again every minute. Only administrators can write there, so users can see the policy but can't change it. When a policy is installed, Settings shows Managed by <organization>; if the file can't be parsed, Settings says so and the last good policy stays in force.
{
// Comments are allowed.
"organization": "Acme",
"network": { "mode": "allowlist", "allowed": ["acme.com", "github.com", "*.atlassian.net"] },
"local-egress": false,
"local-exec": "ask",
"auto-review": true,
"rules": [
{ "kind": "ask", "text": "Before emailing anyone outside acme.com." },
{ "kind": "allow", "text": "Reading public documentation sites." }
],
"disabled-plugins": ["stripe"],
"required-plugins": ["github", "linear"],
"templates": "team",
"team-agents": ["asylum://template/…"],
"cloud-agents": false,
"setup-script": "brew bundle --file=/opt/acme/Brewfile",
"check-script": "test -f ~/.acme/enrolled",
"terminate-inactive-days": 30,
"otel-endpoint": "https://otel.acme.internal:4318"
}
Every key is optional; an empty object — or no file — means unmanaged. Set ASYLUM_POLICY to try a policy from another path before rolling it out.
Deploying it
Any tool that can put a root-owned file on a Mac works: an MDM's custom file payload, a package, or a script.
sudo mkdir -p "/Library/Application Support/asylum"
sudo cp policy.json "/Library/Application Support/asylum/policy.json"
sudo chown root:wheel "/Library/Application Support/asylum/policy.json"
sudo chmod 644 "/Library/Application Support/asylum/policy.json"
Changes apply within a minute, without restarting Asylum. Each change is written to the audit log as policy.applied with the admin as its actor.
Reference
| Key | Type | Effect |
|---|---|---|
organization | string | Shown as "Managed by …" |
network | object | Network Controls: mode, allowed, blocked |
local-egress | bool | false cuts network access for commands run on the user's own Mac |
local-exec | never · ask · always | Ceiling for Execution on Local Computer; stricter wins |
auto-review | bool | Force Auto-review on or off |
rules | array | Locked Auto-review rules: {"kind": "ask"|"allow", "text": "…"} |
disabled-plugins | array | Marketplace ids or names Agents may not use |
required-plugins | array | Marketplace ids added for everyone, which users can't remove |
templates | public · team · off | The widest template sharing allowed |
team-agents | array | Team Agent links added for everyone; they can't be hidden or deleted |
team-agents-enabled | bool | false turns Team Agents off |
cloud-agents | bool | false turns off the Claude Code and Codex CLI providers |
setup-script / check-script | string | Shell run on the computer; see setup scripts |
terminate-inactive-days | number | Delete a computer's data after this many idle days (0: never) |
otel-endpoint | string | OTLP/HTTP collector for every user; overrides theirs |
Marketplace ids: github linear notion jira jiratoken sentry stripe supabase vercel huggingface deepwiki context7 google slack pagerduty brave postgres playwright datadog salesforce figma.
Network Controls
mode | Agents can reach |
|---|---|
open (or unset) | Anywhere |
default | Anywhere except the hosts in blocked |
allowlist | Only the hosts in allowed |
offline | Nothing outside the Mac |
An entry matches the host and its subdomains: acme.com covers docs.acme.com but not acme.com.evil.io; a leading *. is accepted and means the same.
Enforcement doesn't rely on the model behaving. Asylum runs a small filtering proxy on 127.0.0.1:
- the Agents' browser launches through it, so every page, redirect, and script request is checked;
- commands on the computer get it as
HTTP(S)_PROXY, and the sandbox denies them any other outbound connection — a tool that ignores the proxy simply can't connect; - the web search, page fetch, and link preview tools go through it too.
Blocked requests get 403 Blocked by your admin's network policy. Changing the mode relaunches the browser on its next use. Model providers and connectors are Asylum's own connections, not Agent browsing, and aren't filtered — control those with disabled-plugins and your network.
Connectors
A disabled connector shows Disabled by your admin in the Marketplace, can't be added, and is never offered to Agents even if it was added earlier. A required connector is added automatically for everyone and can't be removed; each user still signs in with their own account.
Rules and review
Policy rules appear at the top of Auto-review Rules with a lock ("Required by your admin") and can't be edited or deleted. Users can add their own rules beside them; "Ask first" still wins a conflict. With auto-review set, the switch is locked to your value.
Execution
local-exec sets a ceiling on running commands and reading files on the user's own Mac: a user can be stricter, never looser. Below always, the approval card's Always allow is disabled. local-egress: false runs those local commands in a sandbox with outbound network denied.
Team Agents
List Team Agent links in team-agents and every user gets them, marked Required by your admin — they can't be hidden or deleted. Remove a link and the Agent stays but becomes optional. team-agents-enabled: false turns Team Agents off; users opening a Team Agent link see Team Agents Not Available. templates limits sharing: team allows only team-only links, off none.
Setup scripts
setup-script runs once on each Mac's computer, in its workspace and sandbox, and again only when you change the script. check-script runs every time the computer starts. A non-zero exit sends the user a notification with the output and writes computer.script_failed to the audit log; Agents keep working either way. Scripts are subject to Network Controls like anything else on the computer.
Computers
terminate-inactive-days erases a computer that hasn't been used for that long — workspace files, browser sign-ins, and backups — and logs computer.terminated_inactive. Agents and conversations stay; the computer starts fresh next time. Users can also Recreate, Stop, or Delete computer and data themselves in Settings → Computer.
Activity
Each Mac keeps its own records in its database; nothing is sent anywhere unless you set an OpenTelemetry endpoint or call the Admin API. Users see the same data in Settings → Activity: a week of Insights, the two logs, and export.
Action Recording
One entry per tool call: time, Agent, conversation, tool, target (a URL, path, or recipient — at most 500 characters), outcome, and duration. Outcomes are done, error, denied (by a rule or Auto-review), user-denied, and expired. Arguments and results are never recorded. Entries are kept for 90 days.
Audit log
Control-plane changes, kept indefinitely: settings.changed (which keys, not values), rule.added, rule.deleted, plugin.added, plugin.removed, policy.applied, computer.updating, computer.recovering, computer.recreating, computer.resetting, computer.update_scheduled, computer.setup, computer.script_failed, computer.terminated_inactive, conversation.exported, and conversations.exported. Repeats of the same change within 30 seconds are one entry. The actor is user or admin.
Export
Conversation content export writes each conversation as JSON and Markdown: every message's author, time, and text, plus each tool call's input, output, and status. Users export one conversation from its sidebar menu or all of them from Settings → Activity; the API exports one by id.
{
"id": "…", "title": "Bug Reproducer", "kind": "direct",
"messages": [
{ "at": 1759312440000, "role": "user", "author": "user", "text": "Reproduce #482", "tools": [] },
{ "at": 1759312468000, "role": "agent", "author": "Bug Reproducer", "text": "Bisected to 9f3c2e1.",
"tools": [ { "tool": "shell", "input": { "command": "git bisect run ./repro.sh" }, "output": "…", "status": "done" } ] }
]
}
OpenTelemetry
Set otel-endpoint in the policy (or a user sets Collector endpoint in Activity) and Asylum sends a span for every Agent turn and tool call to <endpoint>/v1/traces as OTLP/HTTP JSON. Spans carry ids and names only — never message text, arguments, or results:
| Span | Attributes |
|---|---|
turn | asylum.agent asylum.chat asylum.origin (chat, routine, handoff…) asylum.outcome |
tool | asylum.agent asylum.chat asylum.tool asylum.outcome |
Every span has service.name = asylum and asylum.surface = asylum.agent. Delivery is best-effort; a collector that's down doesn't slow Agents down.
Admin API
Turn on Local Admin API in Settings → Activity. It listens on http://127.0.0.1:7438/v1 and needs Authorization: Bearer <token>; Copy token copies it from the Keychain, where it's created the first time.
| Request | Returns |
|---|---|
GET /v1/agents | Agents: id, name, status, kind, model |
GET /v1/actions?since=<ms> | Action Recording, newest first |
GET /v1/audit?since=<ms> | Audit log, newest first |
GET /v1/insights?since=<ms> | Counts, outcomes, top tools and Agents |
GET /v1/chats/{id}/export | One conversation's content |
POST /v1/chats/{id}/messages | Send {"text": "…"} as the user; returns the message id |
TOKEN=$(pbpaste)
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:7438/v1/insights | jq
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -d '{"text": "Run the weekly report now."}' \
http://127.0.0.1:7438/v1/chats/<chat-id>/messages
A missing or wrong token gets 401; a bad request 400 with {"error": "…"}. Turning the API off takes effect after a restart.
What isn't covered
Asylum runs entirely on each person's Mac; there is no shared server. That means no SCIM, central group management, invitations, per-group policy overrides, or billing — distribute the policy file and Team Agent links instead.