all docs
/ reference

On-prem Configuration as Code (ace.yaml)

On-prem only: where ace/ace.yaml lives (the client's own git repository, checked out on the gateway host), how to generate the first one from a running gateway, how to point the gateway at it, and how changes roll out after that.

ace.yaml lives in an ace/ directory in the client's own git repository, checked out on the gateway host. The gateway reads that checkout through a read-only mount (ACE_CONFIG_HOST_DIR), and every change after the first is a pull request to it. It holds behaviour only — skill modes and parameters, model access, the llm_router allowlist, admission, SLO — never ACE keys or provider credentials; the schema refuses credential fields by name. Hosted deployments are configured from the dashboard and do not use it.

<client-config-repo>/
  ace/
    ace.yaml        # required
    MIGRATION.md    # written by the export
    env/prod.yaml   # optional overlay, selected by ACE_CONFIG_ENV=prod

1. Generate it from the running gateway. On the gateway host, from the bundle directory. Works in db mode, and in file mode until the first ace.yaml is applied (then 409 config_managed). The console's on-prem MCP server (/mcp/onprem) offers the same as export_ace_config, and the CLI as ACE_ADMIN_KEY=… ace config export --from-live --out <dir>.

set -a; . ./.env; set +a
curl -sS http://localhost:8000/api/v1/config/export \
  -H "x-ace-config-secret: ${ACE_CONFIG_ADMIN_SECRET}" > export.json
# export.json: {files: {"ace.yaml", "MIGRATION.md"}, valid, warnings, notes}

Write files into ace/ in a checkout of the client's repository. valid must be true; each entry in warnings is behaviour the file would not reproduce.

Add the router allowlist. The export never carries llm_router.allowlist (where the model router may route); the model audit decides it. Run llm_router_model_audit — or compute_audit, which includes it — on this docs MCP server with deployment: "onprem" and data naming the workflow's recorded LLM requests (without data it reads the models from the code). Paste its onprem_ace_yaml block into ace.yaml at the top level, beside tenants:. Left empty, the router routes nothing. Do not copy model_access into it: that list is for fallbacks and the router does not read it.

llm_router:
  allowlist:
    bedrock:  # credential channel; models as the client sends them
      global.anthropic.claude-sonnet-5-5: {}  # in use: agent_turn, 77% of calls
      "global.anthropic.claude-haiku-4-5-20251001-v1:0": {}  # downgrade for <call class>: <evidence>
      # an entry takes price: {in, cached_in, out} (USD per 1M) only where the catalog lacks it

The comment on each model is the audit's reason — the call classes and their share of calls for a model in use, or the call class and evidence for a downgrade — for whoever reviews the ace/ pull request. The gateway does not use it; edit or drop it freely. A Bedrock geographic profile id (us., eu., …) is never routed; a global. model routes only to other global. models. After apply, GET /api/v1/llm_router/allowlist reads each entry back with its catalog status; one that is not routable is never routed to.

Tenants, environments and keys. ace.yaml has no per-key level: a key gets the settings of the tenant it was minted under. Environments and clients are tenants (echelon:prod, echelon:sandbox, echelon:dev:client_b), so mint each key under its environment's tenant (mint_dev_key with sub_tenant, or the console). A tenant inherits from the level above it one level at a time; it writes only what differs. Skills, model_access and the router allowlist all resolve per tenant: tenants.<t>.llm_router.allowlist has the top-level list's shape, and the nearest level that sets one replaces the lists above it.

tenants:

  # echelon:dev: inherits echelon
  # keys: dev-ci (k1)
  echelon:dev:
    llm_router:  # set here
      allowlist:
        bedrock:
          global.anthropic.claude-sonnet-5-5: {}
    # Every skill not listed here inherits from echelon.
    skills:
      llm_router:  # set here
        mode: shadow

  # echelon:dev:client_a: inherits echelon:dev -> echelon
  # keys: client-a-ci (k2)
  echelon:dev:client_a:
    # Every skill not listed here inherits from echelon:dev.
    skills: {}

  # echelon:dev:client_b: inherits echelon:dev -> echelon
  # keys: client-b-ci (k3)
  echelon:dev:client_b:
    llm_router:  # overrides echelon:dev: +bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0
      allowlist:
        bedrock:
          global.anthropic.claude-sonnet-5-5: {}
          "global.anthropic.claude-haiku-4-5-20251001-v1:0": {}
    # Every skill not listed here inherits from echelon:dev.
    skills:
      llm_router:  # overrides echelon:dev: shadow -> prod
        mode: prod

The export writes this shape: every tenant, even one with nothing of its own (skills: {}); its lineage and active keys as comments (keys themselves stay in the key registry); and each setting a level writes marked # set here or # overrides <tenant>: <before> -> <after>. A key's router allowlist from the console becomes its tenant's llm_router.allowlist. Every top-level section (deployment, providers, tenants, slo, observability, llm_router) is written even when empty, with its defaults in a comment.

2. Commit, review, merge ace/ace.yaml and MIGRATION.md in the client's repository. In CI: ace config validate --offline and ace config plan --offline.

3. Check out the merged repository on the gateway host (for example /srv/<repo>) and point the bundle's .env at its ace/:

ACE_CONFIG_SOURCE=file                    # the bundle default
ACE_CONFIG_HOST_DIR=/srv/<repo>/ace       # mounted read-only at /etc/ace/config
ACE_CONFIG_ENV=prod                       # optional overlay
ACE_DEPLOY_TOKEN_SHA256=<sha256 of the deploy token>
#   token:  openssl rand -hex 32
#   digest: printf '%s' "$TOKEN" | shasum -a 256

Then docker compose -f docker-compose.enterprise.yml up -d ace-proxy ace-webui. Until an ace.yaml is applied, the gateway serves its stored settings, keys keep working and minting, and behaviour writes return 409 with revision: null.

4. Apply. ACE_DEPLOY_TOKEN=<token> ./deploy.sh apply-config applies the mounted directory with ACE_CONFIG_ENV. The gateway validates and swaps without a restart; a clean checkout sends its commit as the revision. The swap is in memory and a restart reloads the mount, so apply only the mounted checkout — any other directory is undone by the next restart, and apply-config warns.

5. Check. /healthz → config.deployed: true with the commit as revision; ace config drift → in_sync: true; the console's Save reads Propose change.

From then on: pull request to ace/ → merge → git pull in the host checkout → ./deploy.sh apply-config. Never edit the files on the host. Switching back to console editing is ACE_CONFIG_SOURCE=db in .env and a recreate.