# The openeng engine

> Reference for openeng, the OpenEng AI engine: its commands (serve, pull, headless, daemon), the --key, --default and --config flags, its environment variables, the ~/.openeng layout, where its logs go, and how it updates itself. As of September 25, 2026, OpenEng AI isn't live yet.

`openeng` is the OpenEng AI engine. It's one binary that runs on your own machine, runs the models and
agents your published [configs](configs.md) describe, and does the work you start from the Web Console at
[app.openeng.app](https://app.openeng.app). It binds no local port: it dials out to the OpenEng
**Broker**, and the browser reaches your engine through that link. That makes it a different kind of
program from the engineering-app engines (`openeng-terminal`, `openeng-kubernetes` and the others),
which listen on a loopback port on your machine ([How OpenEng works](architecture.md)).

> [!WARNING] Status on September 25, 2026: OpenEng AI isn't live yet
> The Console and Web Console interfaces load, but the OpenEng AI API routes and the Broker aren't
> deployed. Nothing can be saved or published, and no AI engine can register or connect. Pro,
> Enterprise and pay-as-you-go launch in Q4 2026 and can't be purchased yet. This page describes
> OpenEng AI as it works once those services are live ([status](ai.md)).

> [!NOTE] Before you start
> The engine is published for **Linux x86_64** only; on Windows, run it inside WSL.
> [Install](install.md) covers the installer, checksums and uninstalling. The engine starts only
> with an **OpenEng key** (an API key from the Console) and registers only for an account with an
> active **Pro** plan. Pro launches in Q4 2026 and can't be purchased yet
> ([Plans & billing](account.md)).

## Commands

| Command | What it does |
|---|---|
| `openeng` or `openeng serve` | Starts the engine: registers your key, then dials the Broker and serves requests. `serve` is the default command. |
| `openeng pull <role>` | Downloads one model of the embedded suite into `~/.openeng/models` ([below](#pull)). Needs no key. |
| `openeng headless` | The entry point for headless machines ([below](#headless)). |
| `openeng daemon` | The sandboxed model process the engine starts by itself to run local model weights, so they never load inside the serving process. You don't run it yourself. |
| `openeng --help` | Prints the usage summary (`-h` and `help` work too). |

Any other command prints `unknown command` followed by the usage summary, and exits with an error.

There is no `--oauth` flag and no browser sign-in for this engine; the key is the only way it
authenticates. (The engineering-app engines are different: they start with a one-time key from the
app, `openeng-terminal serve --oauth=<one-time key>`; see [Getting started](getting-started.md).)

## Start the engine

```bash
openeng serve --key=<your OpenEng key>

# or keep the key in the environment
export OPENENG_KEY=<your OpenEng key>
openeng serve
```

To get a key, open the Console, go to **Deploy → API keys** and press **Issue key**. The value is
shown exactly once, so copy it then ([API keys & sandboxes](api-keys-sandboxes.md)).

The engine looks for the key in this order: `--key=<key>` (or `--key <key>`), then `OPENENG_KEY`.
With neither, it prints
`cannot start: no OpenEng key (--key=<key> or OPENENG_KEY). Generate one in the Console (Pro required).`
and exits. An empty `--key=` is refused too, with `cannot start: empty --key`.

Once it has a key, the engine:

1. **Registers** the key with the OpenEng API, which checks the key and your Pro plan and sets up the
   engine's Broker session. The key goes to the OpenEng API (registration, config and
   placeholder-value fetches) and, with each update check, to releases.openeng.app
   ([below](#updates)); it never goes to the Broker.
2. **Resolves the config it serves by default** ([below](#config-selection)) and applies it: the
   role-to-model bindings, the storage paths, the resource limits and the config's other settings.
3. **Warms up**: it downloads the models that config needs and aren't on disk yet. If a resource the
   config requires can't be made ready, the engine refuses requests for that config rather than
   serving it partly.
4. **Starts the update check** ([below](#updates)).
5. **Dials the Broker** and serves requests over that outbound link. If the link drops, the engine
   reconnects on its own; if the Broker ever rejects its session, it registers again with your key.

It works in the directory you start it from: file tools and shell commands run there, and files you
attach in the Web Console are saved under `attachments/` in that directory. Attachments reach the
engine inside the turn request, through the Broker. The Broker keeps every envelope it routes
(requests with any attachments, replies and control data) for a retention window, 7 days by default,
then deletes it by age ([Transport security](security.md#ai-transport)).

**One engine per key.** The API ties each key to one running engine. Starting a second engine with the
same key fails with `this key already runs an engine — stop it or generate a new key`. Pro includes
one API key ([Plans & billing](account.md)).

To stop the engine, press <kbd>Ctrl</kbd>+<kbd>C</kbd> in the terminal where it runs.

### Local models need a sandbox

The engine runs local model weights only inside its sandboxed model process (`openeng daemon`). On
Linux, that sandbox needs one of these to work on the machine:

- a live **systemd user session**, so that `systemd-run --user` runs (it also applies the config's CPU
  and memory limits), or
- **bubblewrap** (`bwrap`), with unprivileged user namespaces allowed.

The engine checks that the backend actually runs, not just that it's installed. Desktop Linux usually
has a systemd user session; containers, CI runners and SSH logins without a user session often have
neither. In WSL, enable systemd for the distribution or install bubblewrap.

When the sandbox works, the engine prints
`openeng: local runtime = SANDBOXED daemon (enforced enclosure; weights never load in this process)`
as it starts. When it doesn't, the engine still starts, but it prints
`local model inference refused (no functional sandbox enclosure): …` and every request for a local
model fails with that message. To fix it, install bubblewrap (for example `sudo apt install
bubblewrap`), enable a systemd user session, or run the engine outside the nested container.

Hosted and cloud models, an Ollama server and external-CLI models still work without the sandbox. On
such a host, calls to hosted and cloud models and to Ollama go directly from the engine, and it
prints a one-time warning that they're running `HOST-DIRECT`. Set `OPENENG_SANDBOX_MANDATORY=1` to
refuse those calls instead ([Models](models.md#one-gateway-one-sandbox-boundary)).

### Flags

`openeng serve` accepts these flags, written either as `--name=value` or `--name value`:

| Flag | What it does |
|---|---|
| `--key=<key>` | Your OpenEng key. Takes precedence over `OPENENG_KEY`. |
| `--default=<config id>` | The config used for requests that don't name one. Takes precedence over `OPENENG_DEFAULT_CONFIG`. |
| `--config=<id>,<id>,…` | Preloads these configs at start: the engine fetches each one and downloads any embedded model it references that isn't on disk yet. A config that can't be fetched is logged and skipped. |

A config id is the `<name>_<suffix>` id shown, with a copy button, on each card of the Console's
**Configs** page ([Configs](configs.md#the-configs-list)).

## Which config the engine serves

The engine never authors configuration; it runs what you published in the Console.

- **In the Web Console**, every session starts with you picking a published config and one of its
  versions. The engine fetches that version the first time a session uses it. Published versions never
  change, so to use a newer publish, pick the newer version.
- **Requests that name no config** use the engine's default config, chosen in this order:
  1. `--default=<config id>`
  2. the `OPENENG_DEFAULT_CONFIG` environment variable (the older name `OPENENG_CONFIG_ID` still works
     and prints a one-time warning asking you to rename it)
  3. your account's default: the config you own that is named `default`, otherwise the first config
     you own.
- **With nothing published**, the engine prints
  `NO PUBLISHED CONFIG — refusing to serve turns until a configuration is published` and answers
  every request with *"No configuration published, please create and publish a configuration to start
  accessing the engine."* The Web Console shows the same message until you publish a config
  ([Configs](configs.md#publishing)).

The engine keeps its last resolved copy of the default config in `~/.openeng/config-cache.json`. At
start it reuses that copy if it was fetched in the last 30 seconds, and otherwise checks with the API
first. If the API can't be reached, the engine starts with the copy it has rather than none.

## Embedded models: `openeng pull`

`openeng pull <role>` downloads one model of the engine's embedded suite (a GGUF file from Hugging
Face) into `~/.openeng/models`. It resumes an interrupted download and does nothing if the model is
already there. Without a role it pulls `chat`. The installer runs `openeng pull chat` for you unless
you set `OPENENG_NO_MODELS=1` ([Install](install.md#what-the-installer-does)).

| Role | Model | Download size |
|---|---|---|
| `chat` | Gemma 3 4B Instruct | about 2.5 GB |
| `coding` | Qwen2.5 Coder 3B Instruct | about 2.0 GB |
| `planner` | Qwen2.5 7B Instruct | about 4.5 GB |
| `summarization` | Qwen2.5 1.5B Instruct | about 1.0 GB |
| `classify` | Qwen2.5 0.5B Instruct | about 0.4 GB |
| `semantic-mapping` | Qwen2.5 Coder 3B Instruct (the same file as `coding`) | about 2.0 GB |
| `guardrail` | Qwen2.5 0.5B Instruct (the same file as `classify`) | about 0.4 GB |
| `reviewer` | Gemma 3 4B Instruct (the same file as `chat`) | about 2.5 GB |

Roles that share a file download it once. An unknown role prints
`unknown model role '<role>' — known: …` with the list above. You rarely need to pull by hand: at start,
the engine downloads what its config needs, and `--config` does the same for the configs you preload.
Which model serves which role in a config is set in the Console ([Models & role defaults](models.md)).

## Headless machines

```bash
openeng headless [--driver auto|work-pull|schedulers]
```

`openeng headless` takes the key the same way as `serve` (`--key`, then `OPENENG_KEY`) and picks a
driver: `work-pull` (fleet work), `schedulers` (the cron jobs you publish), or `auto` for both, which
is the default and also what an unknown value falls back to. In the current build the command stops
there: it prints the driver it resolved and exits, without running scheduler or fleet work.

## Environment variables

| Variable | Default | What it does |
|---|---|---|
| `OPENENG_KEY` | — | Your OpenEng key, used when `--key` isn't given. |
| `OPENENG_DEFAULT_CONFIG` | — | The config id for requests that name no config. `--default` takes precedence. |
| `OPENENG_CONFIG_ID` | — | The older name for `OPENENG_DEFAULT_CONFIG`. Still read; the engine warns once. |
| `OPENENG_MODEL` | `chat` | The model the engine serves by default: a model role (`chat` is the embedded chat model) or a model tag, for example an Ollama tag such as `qwen2.5-coder:3b`. |
| `OPENENG_HOME` | `~/.openeng` | The root folder for all of the engine's files ([below](#files)). |
| `OPENENG_<COMPONENT>_DIR` | `<root>/<component>` | Moves one component elsewhere, for example `OPENENG_MODELS_DIR=/mnt/big/models`. |
| `OPENENG_SELF_UPDATE` | on | `0` turns off both the update check and applying a downloaded update. |
| `OPENENG_UPDATE_CHANNEL` | `stable` | The release channel to follow: `stable` or `beta`. |
| `OPENENG_UPDATE_CHECK_SECS` | `21600` (6 hours) | Seconds between update checks. |
| `OPENENG_SANDBOX` | `workspace-write` if a sandbox works on this machine, else `off` | How shell commands the agent runs are confined: `workspace-write` (read anywhere, write only inside the working directory), `read-only`, or `off`. The sandbox is bubblewrap on Linux (Seatbelt on macOS in builds for that platform; there is no macOS AI engine build yet). |
| `OPENENG_SANDBOX_MANDATORY` | off | `1` makes the engine refuse calls to hosted and cloud models and to Ollama when no sandbox works on this machine, instead of sending them directly with a warning. Local models are refused in that case either way ([Local models need a sandbox](#local-model-sandbox)). |
| `OPENENG_AGENT_SHELL` | off | `1` lets the agent run shell commands in a working directory that isn't in the engine's trusted list (`~/.openeng/trust.json`). |
| `OPENENG_RESOURCE_FRACTION` | the config's Resources step | A fraction between 0 and 1 of this machine's CPU and memory. It overrides the published CPU and memory percentages; GPU and storage still follow the config. With no published config, the engine uses 0.10. |
| `OPENENG_MCP_DENY` | — | Comma-separated MCP tool names that are always blocked; this wins over everything ([MCP permissions](mcp.md#permissions)). |
| `OPENENG_MCP_AUTO_ALLOW` | — | Comma-separated MCP tool names that run without asking, whatever their risk class, destructive included. |
| `OPENENG_MCP_ALLOW_WRITE` | off | `1` lets write-class MCP tools run without asking; destructive ones still ask. |
| `OPENENG_ROLE_RULES` | `enforce` | `off` turns [behavioral roles](roles.md#use-in-a-config) off on this machine; `assign` picks roles without applying their rules. |

The installer has its own variables (`OPENENG_BIN_DIR`, `OPENENG_ENGINE_URL`, `OPENENG_NO_MODELS`);
see [Installer options](install.md#installer-options).

## Files: the `~/.openeng` layout

Everything the engine stores lives under one root, `~/.openeng` (`/home/<user>/.openeng` on Linux).
The engine doesn't use the operating system's application-data folders. The engineering-app engines
keep their own files in subfolders of the same root, such as `~/.openeng/terminal`.

Each component's folder is resolved in this order:

1. `OPENENG_<COMPONENT>_DIR`, when it's set and not blank;
2. the path your published config sets for it in the **Global paths** step
   ([Configs](configs.md#global-paths)); an environment variable always wins over the config;
3. `$OPENENG_HOME/<component>`, when `OPENENG_HOME` is set;
4. `~/.openeng/<component>`.

The overrides combine: `OPENENG_HOME=/data/oe` with `OPENENG_MODELS_DIR=/mnt/big/models` puts model
weights on the big disk and everything else under `/data/oe`.

| Component | Default folder | Variable | What's in it |
|---|---|---|---|
| root | `~/.openeng` | `OPENENG_HOME` | Everything below, plus the root files listed after this table |
| models | `<root>/models` | `OPENENG_MODELS_DIR` | Model weights: GGUF files, ONNX model folders and Ollama pull markers, with an `index.json` |
| threads | `<root>/threads` | `OPENENG_THREADS_DIR` | Each chat thread's history, metadata and workspace |
| memory | `<root>` itself | `OPENENG_MEMORY_DIR` | Global learned memory (`memory_v2/`) |
| config-state | `<root>/config-state` | `OPENENG_CONFIG_STATE_DIR` | What each config version has learned: its memory, dynamic skills and adaptation ledger |
| skills | `<root>/skills` | `OPENENG_SKILLS_DIR` | Your user-level skills |
| config | `<root>/config` | `OPENENG_CONFIG_DIR` | `roles.json`, the role-to-model map the engine routes by |
| catalog | `<root>/catalog` | `OPENENG_CATALOG_DIR` | The model catalog and an encrypted key store |
| usage | `<root>/usage` | `OPENENG_USAGE_DIR` | The append-only usage ledger (`ledger.jsonl`) |
| journal | `<root>/journal` | `OPENENG_JOURNAL_DIR` | The gateway journal, one file per thread |
| dashboards | `<root>/dashboards` | `OPENENG_DASHBOARDS_DIR` | Dashboard definitions |
| roles | `<root>/roles` | `OPENENG_ROLES_DIR` | The behavioral-roles pack (not the same as `config/roles.json`) |
| agents | `<root>/agents` | `OPENENG_AGENTS_DIR` | Agent profiles |
| workers | `<root>/workers` | `OPENENG_WORKERS_DIR` | The worker registry |
| subagents | `<root>/subagents` | `OPENENG_SUBAGENTS_DIR` | The specialist catalog |
| research | `<root>/research` | `OPENENG_RESEARCH_DIR` | The research knowledge store, kept when threads are deleted |
| logs | `<root>/logs` | `OPENENG_LOG_DIR` | Reserved for log files ([below](#logs)) |
| runtime | `<root>/runtime` | `OPENENG_RUNTIME_DIR` | How the engine finds its model daemon: socket, locks and handshake. Every process involved needs the same value. |
| cas | `<root>/cas` | `OPENENG_CAS_DIR` | Content-addressed artifact store |
| missions | `<root>/missions` | `OPENENG_MISSIONS_DIR` | Long-running goal runs |
| loops | `<root>/loops` | `OPENENG_LOOPS_DIR` | Loop definitions and run state |
| fleet | `<root>/fleet` | `OPENENG_FLEET_DIR` | Fleet identity and state |
| transmit | `<root>/transmit` | `OPENENG_TRANSMIT_DIR` | Keypairs and grants for secure transmission |
| update | `<root>/update` | `OPENENG_UPDATE_DIR` | A downloaded update waiting for the next start |
| checkpoints | `<root>/checkpoints` | `OPENENG_CHECKPOINTS_DIR` | Checkpoints of resumable runs |
| maps | `<root>/maps` | `OPENENG_MAPS_DIR` | Repository maps |
| web | `<root>/web` | `OPENENG_WEB_DIR` | Documents fetched during web research |
| mcp-tokens | `<root>/mcp-tokens` | `OPENENG_MCP_TOKENS_DIR` | OAuth tokens for MCP servers, readable only by you |
| attachments | `<root>/attachments` | `OPENENG_ATTACHMENTS_DIR` | A cache of delivered attachments |

In the current engine, `OPENENG_MEMORY_DIR`, `OPENENG_DASHBOARDS_DIR` and `OPENENG_WORKERS_DIR` have no
effect yet: nothing in the served engine writes to those tiers.

Files at the root include:

- `config-cache.json`: the last resolved copy of the default config ([above](#config-selection)).
- `entitlement.json`: the last plan information the engine received.
- `ai-config.toml`: engine settings, such as `threshold_pct` under `[complexity]`: how complex a request
  must be before the engine plans it as a multi-step run, written as a percentage. The default, 65, is
  the same threshold as 0.65 on the 0–1 scale the Console uses ([Execution flow](execution-flow.md)).
- `trust.json`: the directories you trust.

Two more places hold engine data. A project you work in can have its own `.openeng/` folder next to
its code, like `.git`, for project-scoped permissions, plans and skills; it always stays with the
project. Temporary scratch files go to the operating system's temp folder.

Variables without the `_DIR` suffix are switches, not folders. For example, `OPENENG_THREADS` sets
the number of CPU threads for local inference; it doesn't move the threads folder.

## Logs

The engine writes its log to standard error in the terminal where it runs; every line starts with
`openeng:`. The current build doesn't write log files, so the `logs` folder stays empty. To keep a
log, redirect it:

```bash
openeng serve 2>> "$HOME/openeng-engine.log"
```

Lines worth knowing:

- `boot auth via OpenEng key (registering with …)`, then `registered engine '<id>' …`: the key and
  your plan were accepted.
- `config resolved — roles=…`: the default config was applied, with its role bindings.
- `warm-up: config <id>@<version> READY (…)`: the models and other resources that config needs are in
  place. `UNAVAILABLE` instead lists the required resources that aren't ready.
- `resource caps (published): cpu …% gpu …% memory …% storage …%`: the limits from the config's
  Resources step.
- `broker transport — dialing …`: the engine is connecting to the Broker.
- `NO PUBLISHED CONFIG …`: publish a config in the Console ([above](#config-selection)).
- `CRITICAL update … applied on disk — RESTART REQUIRED`: restart the engine ([below](#updates)).

## Updates

The engine is built to update itself (as of September 25, 2026 there is no `engine` release to update
to yet; see the end of this section). At start and then every 6 hours (`OPENENG_UPDATE_CHECK_SECS`) it asks
[releases.openeng.app](https://releases.openeng.app) for the latest `engine` release on its channel
(`stable`, or `OPENENG_UPDATE_CHANNEL`) for its platform. Each check sends your OpenEng key to
releases.openeng.app (in the `X-OpenEng-Key` header), whether the key came from `--key` or
`OPENENG_KEY`; `OPENENG_SELF_UPDATE=0` stops the checks, and with them this use of the key. When
there's a newer version, it:

1. downloads it and checks its SHA-256 checksum; a file that doesn't match is never installed;
2. refuses anything that isn't a native executable for this operating system, such as an error page
   or a binary for another platform;
3. stages it in `~/.openeng/update/`;
4. swaps it in the next time you start the engine, after checking the checksum again.

A release marked critical, or a running version below the minimum supported version, is swapped on
disk right away, and the engine prints `RESTART REQUIRED`. It never stops itself in the middle of
work; restart it when you're ready. If the release service can't be reached, the engine keeps running
the version it has and checks again later. `OPENENG_SELF_UPDATE=0` turns all of this off.

As of September 25, 2026, the release service doesn't list an `engine` release yet, so the check finds
nothing to install. To update before then, run the installer again ([Install](install.md#updates)).

## Troubleshooting

| The engine prints | What to do |
|---|---|
| `cannot start: no OpenEng key …` | Pass `--key=<key>` or export `OPENENG_KEY`. Issue a key under Deploy → API keys. |
| `cannot start: empty --key` | The `--key=` value is blank; check the variable you expanded into it. |
| `the API rejected the OpenEng key …` | The key is mistyped or was revoked. Issue a new key. |
| `an active Pro plan is required to run an engine` | The account isn't on Pro. Pro launches in Q4 2026 ([Plans & billing](account.md)). |
| `this key already runs an engine …` | Stop the other engine that uses this key, or issue another key if your plan allows it. |
| `NO PUBLISHED CONFIG …` | Publish a config in the Console ([Configs](configs.md#publishing)). |
| `config resolve failed (…) — serving last-good cached config` | The API couldn't be reached at start; the engine runs your last config and fetches again on the next start. |
| `warm-up: config … UNAVAILABLE — … required resource(s) not ready` | A model or other resource the config needs couldn't be downloaded or found. Check the network and disk space, then restart the engine; the rest of the line names what's missing. |
| `local model inference refused (no functional sandbox enclosure): …` | No sandbox works on this machine, so the built-in local models can't run. Install bubblewrap, enable a systemd user session, or run outside the container ([Local models need a sandbox](#local-model-sandbox)). Hosted, external-CLI and Ollama (catalog) models still work. |
| `… external model calls are running HOST-DIRECT …` | The same missing sandbox: hosted and cloud model calls go directly from the engine. Fix the sandbox, or set `OPENENG_SANDBOX_MANDATORY=1` to refuse them. |
| `unknown model role '<role>' …` | Use one of the roles listed in the message ([above](#pull)). |
| `unknown command '<name>'` | The engine has only the commands [listed above](#commands). |

For problems with the Console or the Web Console, see
[the OpenEng AI quickstart](ai-getting-started.md#troubleshooting) and
[Troubleshooting](troubleshooting.md).
