Skip to content

Enterprise Managed Settings and the Gating Contract

Backend.AI GO can be centrally managed by an enterprise policy. A policy lets an administrator override settings, lock them so users cannot change them, and (from Phase 2) hide pages and settings tabs. This page is the stable, versioned reference for what a policy may target: the managed-settings gating contract.

This document is treated as a public administrator API. The set of policy-controllable settings paths and the valid UI gating ids defined here are validated at policy load and consumed by the in-app gating, so a UI refactor reconciles this contract rather than silently breaking deployed policies.

How a policy targets settings

A policy carries two fields that drive managed configuration:

  • settingsOverrides: an RFC 7386 JSON merge patch applied over the user's settings. Objects merge recursively, scalars and arrays replace wholesale, and an explicit null deletes a key.
  • lockedPaths: a set of dotted setting paths the user may not change. A locked path can carry a value in settingsOverrides (the policy sets and freezes it) or stand alone (the policy freezes the path at its current effective value).

Both fields name settings by their dotted, camelCase path, the same path that appears in the app's settings JSON. For example, tools.denylist, endpoints.modelHub.baseUrl, or advanced.enableRemoteAccess.

There is no separate policy schema for these sections. Managed domain config lives as regular settings fields, so a policy targets a setting through the same merge patch and lock machinery for every domain (endpoints, tools, features, and the rest). This keeps one uniform managed-config contract.

Fail-loud validation

Before a policy contributes to the effective settings, every path it targets is validated. A policy that targets a path this build cannot honor is rejected at load with an actionable error rather than loaded and silently ignored. This matters most for security controls: a deny run_command lock with a fat-fingered path must not leave the tool enabled while the administrator believes it is blocked.

Validation runs in the client (where the real settings schema is known), after the policy document parses and its schema version is checked, for both the machine policy and the verified central policy.

Forward-compatibility rule

A policy authored by a newer build may legitimately target a settings domain this build does not yet know. A typo within a domain this build does know is an authoring mistake. The rule that separates the two:

  • If a path's top-level segment is a settings domain this build knows (for example tools, endpoints, features), every segment of the path must resolve to a real field. Otherwise the policy is rejected. A typo such as endpoints.modelHub.baseUrI (capital I) or tools.denylst fails loud.
  • If a path's top-level segment is a domain this build does not know at all (for example a future quotas section), the path is accepted and skipped. It remains visible in the managed status and begins applying once a build that understands that domain is installed.

UI gating ids are a closed set with no forward-compatibility band: an unknown page id or settings-tab id is always rejected, because the frontend either renders a page or tab or it does not.

Policy-only, read-only-to-user settings

Some settings are governance concepts that only an administrator should set, never an individual user. Such a setting is policy-only: a policy may populate it, but a user-originated settings update that targets it is rejected, not silently dropped. Its un-managed default is inert, so an install with no policy behaves exactly as before.

The policy-only sections are features (page and settings-tab hiding) and deployment (deployment profile and capability gating). The features fields are documented under "Tool governance and feature gating" below; the deployment fields are documented under "Deployment profiles" below.

Contract version

The gating contract is versioned. The current version is 1. The version changes only when the shape of the contract changes (a new kind of gating id, or a changed validation rule), not when an additive settings field or a new page is introduced within the existing shape.

Policy-controllable settings paths

Any leaf or section in the app's settings schema can be targeted by a policy, subject to the forward-compatibility rule above. The settings domains a build understands are exactly the top-level sections of its settings schema. The validator derives the known path set from the running build's schema, so it never drifts from the code.

Representative domains include:

  • general: language, theme, models directory, and related app-level settings.
  • inference: serving ports, default context length, and inference defaults.
  • advanced: logging level, remote access, code-execution sandbox, and related advanced toggles.
  • tools: tool-calling configuration plus the tool allow/deny governance (see below).
  • features: policy-only page and settings-tab hiding (see below).
  • deployment: policy-only deployment profile and capability gating (see below).
  • endpoints: managed service endpoints (update, model hub, registries, the fixed inference endpoint, web search, channels).
  • memory, conversation, notifications, window, favorites, managementApi, defaultModels, acp, containerEngine, mlxEngine, voiceInput.

To lock a whole section, target its section path (for example tools). To lock a single field, target its leaf path (for example tools.maxToolLoops).

Tool governance and feature gating

These two sections are the Phase 2 governance surface.

The tools section is user-configurable and policy-lockable. A user may set these for themselves; a policy locks them through lockedPaths. The governance fields are:

  • tools.allowlist: tool names that are permitted. Empty means all tools are allowed.
  • tools.denylist: tool names that are denied. Deny wins over allow, so a tool in both lists is denied.
  • tools.mcpAllowlist: MCP-server names that are permitted. Empty means all registered MCP servers are allowed.
  • tools.pythonSandboxEnabled: whether the Python sandbox tool is available. Defaults to true.
  • tools.runCommandEnabled: whether the run_command tool is available. Defaults to true.

How the tool allow/deny list is enforced

The global tools allow/deny list is enforced on every tool execution path, not just advertised in the UI. The gate is additional to and authoritative over a per-agent profile's own tool configuration: a global deny overrides a profile that explicitly enables the tool. Enforcement happens in the backend at every execution surface:

  • The agent: both the agent loop (before a tool reaches the built-in registry or an MCP server) and the direct agent tool-execution command and endpoint.
  • The desktop tool-execution command.
  • The headless REST tool-execution endpoint (POST /api/v1/tools/execute).
  • The MCP layer: a server outside a non-empty tools.mcpAllowlist cannot register, and a denied MCP tool is both filtered from the advertised catalog and refused on execution.

Resolution rules: a tool in tools.denylist is denied; the pythonSandboxEnabled / runCommandEnabled toggles fold into the deny set when false; and when tools.allowlist is non-empty, any tool not on it is denied. Tool name aliases (for example run_shell and run_command) are treated as the same tool, so a list entry written in either spelling matches. An MCP tool is referenced by its server:tool name, so a tools.denylist entry of my-server:delete_file denies that one MCP tool.

The gate fails closed: if the effective policy cannot be resolved at execution time, the tool is denied rather than allowed. The un-managed default (empty allowlist and denylist, both toggles true) denies nothing, so an unmanaged install behaves exactly as before. In Settings, a tool denied by policy is shown as locked with a "Managed by your organization" badge (or "Managed by {organization}" when the policy names an organization), distinct from a tool the user has switched off. An un-managed install shows no badge at all.

The features section is policy-only and read-only to the user (a user write to features.* is rejected). Only a policy populates it; the un-managed default hides nothing. Its fields are:

  • features.hiddenPages: a list of page ids to hide from navigation. Each id must be a valid page id (see below).
  • features.hiddenSettingsTabs: a list of settings-tab ids to hide. Each id must be a valid settings-tab id (see below).

Enforcement

Hiding is enforced on the backend, not only in the UI. When a managed policy hides a page, the application rejects that page's API requests and desktop IPC commands, so the page is unreachable even by a user who edits the frontend or calls the API directly. A hidden page's REST routes return HTTP 403, and its backing Tauri commands are rejected over IPC. This is authoritative: removing a nav item is a convenience, and the backend is the control. An un-managed install hides nothing and rejects nothing, so its behavior is unchanged.

The two transports reach the page from different angles. REST gating is prefix-based: a hidden page's route subtree is rejected wholesale at the route-scope middleware. IPC gating is per-command: each page declares the user-facing and security-sensitive Tauri commands that back it, and a hidden page rejects all of them. The per-command list is maintained as close to the REST prefix coverage as the per-command model allows, and a build-time coverage test fails if a command backing a page is left neither gated nor consciously exempted, so the IPC surface cannot silently drift open as new commands are added.

A command that backs more than one page follows the shared-command rule: it is rejected only when every page that surfaces it is hidden, not when any one of them is. The loaded-models surface is the canonical case: the command that lists loaded models backs both the Models and the Sessions pages, so hiding Sessions alone leaves it callable for a still-visible Models page, and it is rejected only once both pages are hidden. This keeps page hiding from breaking a still-visible page that shares a command.

A small set of commands behind a page stays callable on purpose even when the page is hidden: shared inference and pool infrastructure that every model-running page relies on, the runtime memory-injection and chat memory pipeline shared with still-visible chat and plugin pages, directory and maintenance helpers, plugin-runtime storage, and the access-key bootstrap commands that run on startup before their page exists. Gating any of these would break a still-visible page or the application's own startup, so they are exempt by design rather than by oversight, and the coverage test asserts that exemption list stays accurate.

For a settings tab, the same applies to any dedicated routes the tab owns (for example the connectors, managed-endpoints, supervisor, acp, agent-registry, and nodes tabs). Tabs that read and write only through the shared settings endpoint are removed from the UI but share that one endpoint; to make such a tab read-only rather than hidden, lock its underlying settings paths instead (see below).

Managed affordances and audit

Across the governance surface, a managed install shows a consistent read-only affordance and records every blocked action.

In the UI, a setting an administrator controls is disabled and carries a "Managed by your organization" badge (or "Managed by {organization}" when the policy names an organization). A tool denied by policy renders locked in Settings with the same badge. Hidden pages and tabs are simply absent from navigation, with no dead links. An un-managed install shows none of these affordances and adds no rendering overhead.

In the backend, every gated or blocked action produces exactly one structured audit record on the existing audit channel (tracing target = "audit"), as an authz_denial event with reason = "policy_gated". The record names the action (the tool, command, or route path), the path that rejected it (agent, tauri, rest, or mcp), and the deciding policy (the organization, when named). This shares the channel scope denials already use, so one filter catches both. It records at the outermost enforcement choke point of each transport, so a single blocked action is one record, not one per layer.

Deployment profiles

The deployment section is policy-only and read-only to the user (a user write to deployment.* is rejected, not silently dropped). Only a policy populates it. The un-managed default is the standard profile with every capability allowed and no model restriction, so an install with no policy behaves exactly as before.

The section drives the install's deployment shape and what a user may do:

  • deployment.profile: standard (the default, un-managed behavior) or fixed_endpoint (the install talks only to a single managed inference endpoint).
  • deployment.allowedModelIds: a list of model ids the install is restricted to. Empty (the default) means unrestricted. This is a soft restriction independent of the profile.
  • deployment.allowLocalServing: whether a user may start a local inference server. Defaults to true.
  • deployment.allowProviderConfig: whether a user may add or edit upstream providers. Defaults to true.
  • deployment.allowModelDownload: whether a user may download models. Defaults to true.

A fixed_endpoint deployment points at the endpoints.inferenceApi service endpoint, an OpenAI-compatible serving URL. That URL and its credential are an ordinary managed service endpoint, not bespoke deployment config: the URL lives in endpoints.inferenceApi.baseUrl (or mode = disabled), and the credential is written write-only to the OS keychain, never stored in settings or echoed back. The deployment section references the endpoint kind; it does not carry the URL or secret. An administrator running a fixed-endpoint deployment typically pairs profile = fixed_endpoint with allowLocalServing, allowProviderConfig, and allowModelDownload all set to false, plus an endpoints.inferenceApi in custom mode, so the install only ever reaches the managed endpoint.

Fixed-endpoint enforcement

When deployment.profile = fixed_endpoint, the install pins inference to the managed endpoint without bypassing the local router. The chat path is unchanged: every model call still flows through the local continuum-router, which preserves model-alias rewriting, web-search injection, health checks, and retry. At startup the app pre-provisions exactly one OpenAI-compatible router backend pointed at the resolved endpoints.inferenceApi URL; its API key is read from the keychain and passed to the router as an environment-variable reference, so the plaintext key never lands in the on-disk router config. Switching the profile back to standard removes that backend.

The capability flags drive what is hidden and what is rejected. Frontend hiding is a convenience; the backend always enforces the same decision over both transports (the Tauri command guard and the REST route gate), and a managed install fails closed:

  • allowLocalServing = false: on-demand model loading is rejected on both transports. The router and the pinned fixed backend stay up; only local serving stops. When paired with profile = fixed_endpoint, the Models and Engines pages are also hidden (the policy author does not list them; they are derived from the profile and unioned into the effective features.hiddenPages).
  • allowModelDownload = false: the model-download surface is rejected. The /hf REST subtree and the download-initiating commands (download_model, batch_download_model, download_hf_repo, retry_download) are blocked; browsing an already-installed catalog and managing the HuggingFace token still work.
  • allowProviderConfig = false: the upstream-provider configuration surface is rejected. The /providers REST subtree and the provider-management commands are blocked, and the Providers tab is hidden from the API page. The rest of the API page (routing, health, security, keys) is unaffected.

Every blocked action records exactly one policy_gated audit entry naming the action, the transport, and the deciding policy. The flags are independent of the profile: a standard install may set any of them as a soft restriction, and a fixed_endpoint install may leave any of them at true.

Air-gapped mode

egress.airGapped is a single fail-closed master switch. Setting it to true in an applied policy disables, in one flip, every outbound network path except the policy-allowed internal hosts. It sits above the per-endpoint controls (the egress firewall and per-endpoint disabled mode): it forces the firewall on and closes the vectors those controls do not reach.

When air-gapped:

  • The egress firewall is forced to enforce (egress.enforce is treated as true regardless of its written value). An empty or internal-only egress.allowedHosts is honored as-is, so the install reaches only the hosts an administrator explicitly lists.
  • Every service endpoint resolves to disabled, so a default-mode endpoint can never fall back to its public built-in URL (model hub, engine and runtime registries, web-search providers, channel connectors, the update feed, the inference API).
  • The bare-client network vectors that run outside the endpoint resolver are refused before any socket opens, each with a clear "disabled in air-gapped mode" error: the OAuth device flows (OpenAI Codex, Google Calendar), the continuum-router version probe, the agent URL-import tool, the http_request agent tool, and the agent / squad-template catalog registry.
  • Cloud-provider OAuth device-flow sign-in is blocked and providers fall back to API-key authentication. The OAuth device-flow entry points are hidden in the UI.
  • Anonymous telemetry is forced off and locked: the toggle is disabled with a "Managed by your organization" badge, and a write that tries to enable it is rejected.
  • The in-app updater and its "check for updates" affordance are suppressed in the UI.

The status surface reports it: get_managed_status (Tauri) and GET /api/v1/enterprise/status (REST) both carry egress.airGapped. The flag is read-only to the user; only a policy sets it. The recommended placement is on egress, since the firewall is installed first at startup and is the master no-egress guarantee. A pre-existing policy that omits the field deserializes as not air-gapped, so an install with no policy behaves exactly as before. A full operator write-up follows in a later revision.

Valid UI gating ids

These ids are the closed set a policy may use to hide UI. They mirror the application's navigation and settings definitions; a drift test keeps this list in lockstep with the code.

Page ids

A policy hides a page by listing its route id. The valid page ids are:

  • /home
  • /dashboard
  • /models
  • /sessions
  • /chat
  • /draw
  • /translation
  • /agent-extension
  • /cowork
  • /schedules
  • /squad
  • /autonomous-agents
  • /creations
  • /statistics
  • /benchmark
  • /logs
  • /connector-audit
  • /engines
  • /api
  • /plugins
  • /data
  • /settings

Settings-tab ids

A policy hides a settings tab by listing its tab id. The valid settings-tab ids are:

  • general
  • ui
  • models
  • generation
  • tools
  • voice
  • memory
  • demo
  • supervisor
  • policies
  • acp
  • agent-registry
  • connectors
  • managed-endpoints
  • organization
  • advanced
  • nodes
  • maintenance

Hiding a settings tab removes it from the UI and rejects any dedicated routes it owns (see "Enforcement" above). To make a tab's settings read-only instead of hidden, lock the underlying settings paths; the inputs become disabled and show a managed badge.

Example policy

A policy that forces remote access off, locks it, and disables the model-hub endpoint:

{
  "schemaVersion": 1,
  "policyId": "example-corp-baseline",
  "issuedAt": "2026-06-24T00:00:00Z",
  "egress": { "enforce": false, "audit": false },
  "settingsOverrides": {
    "advanced": { "enableRemoteAccess": false },
    "endpoints": { "modelHub": { "mode": "disabled" } }
  },
  "lockedPaths": ["advanced.enableRemoteAccess", "endpoints.modelHub.mode"]
}

A fixed-endpoint deployment that pins the install to an internal inference server and disables local serving, provider config, and model download:

{
  "schemaVersion": 1,
  "policyId": "example-corp-fixed-endpoint",
  "issuedAt": "2026-06-24T00:00:00Z",
  "egress": { "enforce": false, "audit": false },
  "settingsOverrides": {
    "deployment": {
      "profile": "fixed_endpoint",
      "allowedModelIds": ["internal/llama-3"],
      "allowLocalServing": false,
      "allowProviderConfig": false,
      "allowModelDownload": false
    },
    "endpoints": {
      "inferenceApi": { "mode": "custom", "baseUrl": "https://models.internal/v1" }
    }
  },
  "lockedPaths": [
    "deployment.profile",
    "deployment.allowLocalServing",
    "deployment.allowProviderConfig",
    "deployment.allowModelDownload",
    "endpoints.inferenceApi.mode",
    "endpoints.inferenceApi.baseUrl"
  ]
}

The fixed endpoint's API key, if the server requires one, is set write-only through the managed-endpoints credential command, never in the policy file.

A machine policy is placed at an administrator-writable path (for example /etc/aigo/policy.json on Linux) and is trusted by that path. A central policy is fetched over the network and is trusted by its Ed25519 signature.

Offline license gate

A managed install can additionally require a signed offline license that proves enterprise entitlement from a file alone, with no network access. The license is a JSON document at <app_data>/enterprise/license.json carrying a License (schema version, license id, organization, issue date, optional expiry, optional seat / device limits, and optional entitlements) plus a detached Ed25519 signature. It is signed by the same trust anchors distributed through the provisioning profile, and is verified entirely offline at startup and again every time the status surface is read, so expiry takes effect without a restart.

The license gate is independent of policy enforcement, by design. An absent, expired, invalid, or over-limit license never unlocks the policy locks or the egress firewall: doing so would be a downgrade attack (drop the license file and the restrictions fall away). Instead, the policy locks and the egress allowlist stay fully enforced, and the non-valid license is surfaced as a prominent, localized "license expired / missing, contact your administrator" banner that gates continued enterprise operation.

The license state is reported in the license field of the enterprise status (Tauri get_managed_status and GET /api/v1/enterprise/status, from one shared service). The field is omitted for an un-managed install. For a managed install it carries one of:

  • valid: verified, unexpired, and covers this install. Enterprise operation is enabled.
  • expired: the signature verified but the license's expiresAt is in the past.
  • over_limit: verified and unexpired, but a seat or device limit cannot cover this install. Org-wide seat and device counts are tallied by the management server; the offline gate only enforces the floor that the license must cover at least this single install (a limit below one, or a device-limited license on an install with no device identity).
  • invalid: a license file is present but failed verification (bad or forged signature, an out-of-range schema, an unknown or expired trust anchor, or an unreadable file).
  • absent: no license file is present.

Only valid permits continued enterprise operation; every other state shows the gated banner. The license signature and any key material are never returned in the status (it is secret-free by construction).

An example signed license file:

{
  "license": {
    "schemaVersion": 1,
    "licenseId": "acme-2026",
    "organization": "Acme Corp",
    "issuedAt": "2026-01-01T00:00:00Z",
    "expiresAt": "2027-01-01T00:00:00Z",
    "seatLimit": 250,
    "deviceLimit": 250,
    "entitlements": ["premium_models"]
  },
  "signature": "<base64 Ed25519 signature over the canonical license bytes>",
  "anchorId": "acme-policy-anchor"
}

Provisioning and enrollment

A managed install is bound to an organization by a provisioning profile. Backend.AI GO discovers the profile on launch and, when one is present, enters managed mode: the egress firewall, locked settings, license gate, and the central-policy poll loop all activate. There are three ways to provision a device, and all converge on the same backend (device identity + the one-time enrollment handshake + profile discovery).

Opening an aigo://enroll?token=<one-time-token>&url=<management-server-url> link routes the user to the Organization settings tab with the enrollment dialog pre-filled. The token is a single-use bearer: it is never persisted and is consumed once by the enrollment handshake. Either field may be omitted (the URL can come from a dropped provisioning file; the token can be typed in).

2. Manual enrollment (Organization settings tab)

Settings -> Organization lets a user paste a one-time token and a management server URL. Before the handshake runs, the dialog shows the server URL and any known trust-anchor fingerprints and requires an explicit "trust this server" confirmation. After enrolling, the tab shows the device identity (device id + public-key fingerprint), the org / enrollment status (organization, license state, locked-setting count, egress posture, air-gapped indicator), and a manual "refresh policy" action.

3. Installer / MDM provision.json drop

For zero-touch deployment, drop a provision.json at the OS managed path; Backend.AI GO discovers it on next launch:

  • Linux: /etc/aigo/provision.json
  • Windows: %ProgramData%\aigo\provision.json
  • macOS: /Library/Application Support/ai.backend.go/provision.json

The installer hooks (.deb postinst, the NSIS installer hook, and a macOS .pkg postinstall) prepare the managed directory and install a staged provision.json when one is baked into the package; they never auto-manage a stock install. The template is packaging/provisioning/provision.example.json. The profile uses the same camelCase schema as the central policy (schemaVersion, mode, organization, managementServerUrl, trustAnchors, pollIntervalSecs, optional enrollment.token). Any one-time token in the file is consumed at first launch and stripped from the on-disk cache, so it is never durable.

{
  "schemaVersion": 1,
  "mode": "managed",
  "organization": "Acme Corp",
  "managementServerUrl": "https://policy.acme.example",
  "trustAnchors": [{ "id": "acme-policy-anchor", "publicKey": "<base64 Ed25519 public key>" }],
  "pollIntervalSecs": 900,
  "enrollment": { "token": "<one-time enrollment token>" }
}

Detailed MDM delivery steps (Intune, Jamf, GPO), the installer artifacts, re-enrollment, and token rotation are covered in Device Enrollment and Trust Anchors. For the full operational story, see the Administrator Guide and the rest of the enterprise deployment section: Policy Server Deployment and Operation, Deployment Models, Air-Gapped Deployment, Offline Licensing, Fixed-Endpoint Deployment, and Audit and Compliance.