# Security & privacy

> What OpenEng guarantees about your data, your secrets and your machines across the whole platform: the engineering apps' loopback engines, the OpenEng AI engine and the Broker, ${ENV} placeholders, release integrity, and how to report a vulnerability.

OpenEng is local-first by architecture and placeholder-only by rule. This
page states what the platform guarantees about your data, your secrets, and your machines, and
where those guarantees stop today. Statements about OpenEng AI describe the design until its services
are live; the engineering apps' statements describe the shipped engines.

It covers both halves of the platform: the five **engineering apps** (Terminal, Kubernetes, Data, Git
and Chat), whose engines listen only on your machine's loopback address, and **OpenEng AI**, whose
engine opens no port at all and dials out to the OpenEng Broker. [How OpenEng works](architecture.md)
describes both designs in full.

## Local by default

**Engineering apps.** The web app only renders. Its engine, a native binary on your machine, does the
work: it runs your shells, talks to your clusters and databases, and reads your repositories. The one
exception is Git's CI & CD view, which fetches runs from your forge (GitHub, GitLab, Bitbucket or
Gitea) directly in the browser, with a token stored only in that browser ([CI & CD](git.md#cicd)).
Credentials such as kubeconfigs, database passwords, SSH keys and cloud keys are read by the engine on
your machine and never sent to an OpenEng service. On its own, an app engine contacts OpenEng
at `api.openeng.app`, to exchange its pairing key, renew its sign-in and fetch the certificate
for `engine.openeng.app`, and `releases.openeng.app`, to check for updates (the current macOS builds, which are older releases, still use the release service's former address; the next macOS builds move to releases.openeng.app), and, when an update exists, downloads it from `openeng.app`. The Terminal engine
also downloads an on-device embedding model (BGE-small-en-v1.5, about 130 MB) once from
`huggingface.co` into `~/.openeng/terminal/state/models`, for its semantic command suggestions; it
sends none of your command history ([Suggestions](terminal.md#suggestions)). Everything else an app
engine connects to is something you pointed it at. Each engine keeps its files in `~/.openeng/<app>/`.

**OpenEng AI.** Conversations, thread memory and code maps live under `~/.openeng/` on your machine;
the agents, configs and other items you author in the Console are stored by the OpenEng API. Chat and
coding run on on-device models by default. Your prompts, your threads and the engine's output leave
your machine along these paths:

- **The Broker**, whenever you work from the Web Console. Your requests (with any attachments), the
  engine's replies, and the engine data the Web Console shows you (your thread list and history,
  learned memory, the running config, placeholder names, and loop, warm-up and device status) pass
  through it. The Broker can read everything except the streamed replies to a turn (when the request
  carries a usable browser key), and it keeps every envelope it routes for a retention window, 7 days
  by default ([Transport security](#ai-transport)).
- **The Cloud Planner**, if you pick it for the planner role: the planner's requests go through
  `api.openeng.app` to Gemini on Vertex AI and are paid from your planning credit
  ([Plans & billing](account.md#planning-credit)).
- **Destinations you wire**, each an explicit entry you author: a hosted or external model, an
  external coding CLI, an MCP server, a cell, an integration, or an external plugin endpoint, which
  receives each call it guards (up to the full payload, per its payload policy;
  [Plugins](plugins.md)). The integrations catalog's entries point at OpenEng-hosted endpoints under
  `connectors.openeng.app/fn/`, so their tool arguments would go to OpenEng
  ([Integrations](integrations.md)).
- **Research**, which fetches web pages from the sites it researches.
- **Human-in-the-Loop** (Pro): each approval request goes to `api.openeng.app` for the Console queue.
  It carries the run, thread and config ids, the target and operation, and the rule's message, not
  your prompt or the engine's output ([Human-in-the-Loop](human-in-the-loop.md)).

As of September 25, 2026, the OpenEng AI API and the Broker aren't live yet, so this describes the
design ([status](ai.md)).

## Transport security

### Engineering apps: a sealed loopback channel

- The engine binds `127.0.0.1` only (Terminal 50052, Git 50053, Kubernetes 50054, Chat 50056, Data
  50057), so nothing outside your machine can connect to it.
- The browser reaches it at `https://engine.openeng.app:<port>`. That public name resolves to
  `127.0.0.1`, and at sign-in the engine fetches a publicly trusted certificate for it, so the
  connection is HTTPS with no manual trust step. Every signed-in engine receives the same certificate
  and private key, so TLS here proves the name, not which engine answers; what keeps the connection on
  your machine is that the name resolves to `127.0.0.1`.
- On top of TLS, each request and response frame of a session is sealed on its own, with a fresh
  ephemeral key per frame (X25519, HKDF-SHA256, ChaCha20-Poly1305), so it's encrypted on the loopback
  connection. The engine seals a session only when the client offered a key at `CreateSession`, a
  request frame that fails to unseal is passed on as it arrived, and the key exchange that opens a
  session isn't authenticated ([A sealed transport](architecture.md#sealed-transport)).
- A handshake token, minted when the engine starts, authorizes every call. The engine writes it to a
  file only you can read (`0600`), but it also hands it out at `GET /openeng/handshake` to its own
  app's web origin and to any local program that sends no `Origin` header. On a shared machine, a
  program run by another user can therefore obtain it and drive your engine.
- In the current Linux builds of the Terminal, Kubernetes and Data engines, the engine accepts browser
  requests only from its own app's web origins (its openeng.app host, for example
  `https://terminal.openeng.app`, and the app's Firebase default domains `<site>.web.app` and
  `<site>.firebaseapp.com`), and only when they're addressed to its loopback names. That shuts out
  other web pages and DNS-rebinding tricks
  ([Terminal: origin and Host lockdown](terminal.md#origin-and-host-lockdown-drive-by-defense)).
  The macOS builds are older releases pending a rebuild and don't enforce this exact-origin allow-list
  yet, and in the Chat and Git engines (not published yet) the lockdown is still pending.

> [!WARNING] Older macOS engine builds answer any website
> An engine build without the exact-origin allow-list answers every web origin, so any website you
> visit while it runs can fetch its handshake token and drive it: run your shell, your cluster
> commands or your queries. On macOS, don't browse untrusted sites while an app engine runs, or run
> the engine on a Linux machine until the macOS rebuild ships.

### OpenEng AI: the engine dials the Broker

> [!WARNING] Status on September 25, 2026
> This describes the design. The OpenEng AI routes of the OpenEng API and the Broker aren't live yet,
> so no engine can register or connect today ([OpenEng AI status](ai.md)). The engineering apps'
> transport above works as described.

- The `openeng` engine opens no network port (its model daemon talks to it over a private Unix
  socket). It registers with `api.openeng.app` using your OpenEng key, then keeps an outbound
  connection open to the Broker. The key itself is never sent to the Broker.
- **Browser to engine:** your requests, with any attachments, travel encrypted to the Broker, which
  opens them and re-seals them to route them to your engine. On this leg the Broker is a trusted part
  of the path, not a blind relay.
- **Streamed replies:** the engine's streamed replies to a turn are sealed end to end to your
  browser, so the Broker can't read them. That needs a usable browser key on the request; a run
  without one, such as a headless run, seals its replies to the Broker instead.
- **Engine data:** the engine data the Web Console loads through the Broker (your thread list and
  history, learned memory, the running config, placeholder names, and loop, warm-up and device
  status) is sealed to the Broker, which opens it to deliver it.
- **Retention:** 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, whether or not it
  was delivered.
- The browser and the engine learn each other's public keys through the Broker, so the end-to-end
  sealing of streamed replies also relies on the Broker passing those keys on faithfully.
- Placeholder values you enter in the Web Console go to the OpenEng API over HTTPS and never pass
  through the Broker ([below](#secrets-env-placeholders-end-to-end)).

### The platform APIs

- Console ↔ platform API traffic runs over **HTTPS**.
- engine ↔ platform traffic (registration, config resolution, plan checks and placeholder values)
  targets `https://api.openeng.app`. The AI engine's update check goes to `releases.openeng.app` and
  carries your OpenEng key too ([Release integrity](#release-integrity)).
- The Console refuses to publish a credential over plain `http://` to a non-loopback host, and the
  engine skips such MCP servers and cells at runtime. The API doesn't repeat that check, and the
  engine doesn't apply it to a plugin's endpoint.
- Like the rest of OpenEng AI, these API checks describe the design: the OpenEng AI routes of the
  OpenEng API aren't served as of September 25, 2026 ([status](ai.md)).

## Secrets: `${ENV}` placeholders, end to end

The single most important rule on the platform:

- **The Console stores only placeholder names.** Anywhere a credential
  appears — model credentials, cell keys, MCP auth, storage-point tokens —
  the value is an environment-variable placeholder like `${PROD_CELL_KEY}`.
  Editors refuse raw values client-side, and the API independently rejects
  credential-shaped fields holding anything but a placeholder. The Console and the API hold no value
  for a variable you keep only in the machine's environment. Anyone who controls your account can
  still run work on your engines, which can read that environment, so protect the account as you
  would the keys.
- **Values resolve on the node when a config is resolved** — first from that machine's environment,
  then from any value you stored for the config in the Web Console (below); a stored value you change
  applies at the next config resolution, when a new session starts. Values are sent only to the
  endpoint the published config names for them (your cell's gateway, your model's API).
- **A missing variable disables the owning capability** — the engine reports which variable to set
  (for example `cell "prod-logs" disabled — set PROD_CELL_KEY to enable it`); a blank or partial
  secret is never sent.
- **The engine doesn't upload environment values** — it never sends them to the platform or
  includes them in telemetry, and `floor:secret-egress` blocks resolved values (8 characters or
  longer) from model, MCP, cell, search and connector calls. Shell commands your agents
  run inherit the machine's environment, except `OPENENG_KEY`, which the engine removes, so confine
  them with `OPENENG_SANDBOX` ([Environment variables](engine.md#environment)).
- **Values stored for a config are optional and write-only.** The Web Console's **Placeholder
  values** screen lets you give a placeholder a value instead of exporting it on the machine. The value
  goes to the OpenEng API over HTTPS and is stored encrypted. A browser can set or clear a value and see
  which names are set, but can't read a value back; only a caller holding one of your API keys (normally your engine) can
  fetch it — treat every API key as able to read your stored values. The engine keeps its copy encrypted in `~/.openeng/env-vault.json`. A variable set
  on the machine always wins.

## The always-on gateway floor

Every invocation on your machines — model calls, worker actions, MCP/cell
invocations, sandbox jobs, search — passes through a policy gateway whose
**floor is always on and cannot be edited or shadowed**
([details](govern.md)):

- `floor:secret-egress` blocks resolved secret values from leaving toward external destinations;
  `floor:secret-redact` redacts resolved secret values (your placeholders' values, 8 characters or
  longer) from responses. Both match the values the engine resolved; they don't detect other
  secret-shaped text.
- `floor:authz` authorizes every action; `floor:envelope` sanity-checks
  every payload; `floor:meter` meters token and size use on every call (it
  records, never blocks). Budgets are set with [token limits](govern.md#token-limits)
  and a config's [token caps](configs.md#token-caps).

Your own guardrails (injection screens, redaction, groundedness) stack on
top — the floor applies to every account on every plan.

## Cells and connectors: your cloud, your IAM

A cell runs **inside your cloud account** under the function's own IAM
role / service account. OpenEng never holds your cloud credentials — the engine
calls the cell's HTTPS gateway with an API key that *you* issued in *your*
cloud, resolved on *your* engine like any other placeholder. The engine sends that key as an
`Authorization: Bearer` token, while the cell deploy guides set the gateway up to check an `x-api-key`
header, so until the two are aligned a cell registered in the Console is likely to be refused with
401 or 403 ([Cells](cells.md)). Read/write separation is
enforced by type: a write cell is callable only in an execution (goal) context, never in read-only
analysis ([Cells](cells.md)).

The engineering apps' [connectors](connectors.md) follow the same model: pure-Python source you deploy
in your own cloud behind an API-key gateway, running under the role you give it. A connector is
read-only unless a request carries a valid write key, and the function stores only a salted hash of
that key, so reading its environment doesn't grant writes
([Read-only by default](connectors.md#read-only-by-default-per-request-write-key)). The app engines
keep a connector's API key, bearer token and write key on your machine. Terminal keeps them
write-only. Kubernetes and Data return them to the app over the sealed loopback to pre-fill the edit
and re-auth dialogs: Kubernetes the API key, bearer token and write key, and Data the API key and
bearer token (and a direct connection's DSN).

## Accounts, keys & sessions

One account (Google via auth.openeng.app) spans every surface: the five engineering apps, the Console,
the Web Console and billing.

- **Engineering app engines** start with a one-time pairing key that the signed-in app mints for you
  (`openeng-terminal serve --oauth=<one-time key>`). A key works once and expires after about five
  minutes. The OpenEng API mints at most five unused keys an hour per account, shared by all the apps
  (a key an engine has used no longer counts), and each app also counts the keys it mints in your
  browser. The engine exchanges the key
  with `api.openeng.app` for its own access token, valid for an hour and renewed with a rotating refresh
  token. Every new start of an engine needs a fresh key ([Pairing](architecture.md#pairing)).
- **The AI engine** authenticates only with an OpenEng key, an API key you issue in the Console. There
  is no browser sign-in and no device code: the engine never prompts and never opens a browser. The API
  registers a key only for an account with an active Pro plan, and one key runs one engine at a time
  ([The engine](engine.md#start)).
- **API keys** are shown once at issue; only their SHA-256 hash is stored, and lists show metadata
  only. Revoking a key stops it from registering an engine or calling the API. An engine already
  connected keeps its Broker session until that session expires (up to 24 hours), so stop the engine
  as well. Headless and sandbox machines are
  API-key-only and plan-limited (Free 0 · Pro 1 · Enterprise unlimited, or a number set on the account;
  [Plans & billing](account.md#plans)).
- **Per-user isolation.** Items you create in the Console carry your user id in their id
  (`<prefix>_usr_<uid>_<suffix>`, see [IDs](catalogs.md#ids)). Once the OpenEng AI API is live, it
  checks on every read and write that an item is yours or shared with you, and filters lists to those
  items. Sharing is the deliberate exception to isolation: a config you share by email becomes
  readable by that account ([Configs](configs.md)), and a template you make public can be read by
  everyone ([Templates](templates.md)).

Sign out from each surface's menu; signing out ends that surface's session only. It doesn't stop a
running engine: stop the engine to end its session, and revoke API keys from the Console.

## Sandbox fleet boundaries

- By design, sandbox nodes boot **API-key-only**, with no interactive auth in that mode; as of
  September 25, 2026 no node can register yet. Keys are stored hashed (shown once at issue), bound 1:1
  to a node, and revocable. Revoking a key stops it from registering an engine or calling the API; an
  engine already connected keeps its Broker session until that session expires (up to 24 hours), so
  stop the engine as well. Scopes are
  recorded but not enforced yet; every key has full account authority
  ([API keys & sandboxes](api-keys-sandboxes.md)).
- In the current engine build, `openeng headless` checks its key, prints the driver it resolved and
  exits. It doesn't register a node or run fleet jobs ([The engine](engine.md#headless)).

## Release integrity

OpenEng publishes a SHA-256 checksum for every engine binary, and the installers and updaters check
it.

- **Installers** check each binary against the SHA-256 checksum published next to it (the same URL
  plus `.sha256`) when they can fetch that checksum and a hash tool (`sha256sum` or `shasum`) is
  installed; they print `checksum verified` when they did, and delete a download that doesn't match
  and stop. If the checksum can't be fetched or no hash tool is found, the installer installs the
  binary without checking it and doesn't warn you. The checksum confirms the download matches the
  file openeng.app serves, which catches a corrupted or truncated download. It isn't a signature: it
  comes from the same place as the binary, and the beta engines aren't code-signed. If you don't see
  `checksum verified`, check it yourself ([Install](install.md#verify-a-download)).
- **App engines** (Terminal, Kubernetes, Data) check for an update at every start and every 30 minutes
  while they run. A release without a valid `sha256:` checksum is never installed, a download whose
  checksum doesn't match is rejected, and the new binary must report the exact version the release
  names before it's swapped in. These updates can't be turned off; if a check fails, the engine keeps
  running the version it has ([Updates](install.md#updates)).
- **The AI engine** checks for an update at start and every 6 hours, and each check sends your
  OpenEng key to releases.openeng.app. It checks the checksum when it downloads an update and again
  before it applies it at the next start, and refuses anything that isn't a native executable for
  your system. Set `OPENENG_SELF_UPDATE=0` to turn its updates off, which also stops the checks that
  carry the key ([The engine](engine.md#updates)). As of
  September 25, 2026 the release feed has no `engine` release, so there is nothing for it to download
  yet; run the installer again to update.
- **Release records** on releases.openeng.app hold only pointers and checksums; the binaries are served
  from openeng.app. The release pipeline checks the newest record for each engine against the binary
  it serves and withholds that engine's feed on a mismatch; older records aren't re-checked
  ([The release feed](catalogs.md#release-feed)).
- **The behavioral-roles pack** that the AI engine downloads from releases.openeng.app is signed
  offline with Ed25519 (minisign), and the engine verifies that signature against a public key built
  into the binary before it trusts the pack's checksum.

## Payments

Billing runs on Razorpay's hosted checkout and invoices. The platform
stores subscription state and payment metadata only — **never card
details** ([Plans & billing](account.md)). Razorpay's notifications about your subscription are
signature-checked (HMAC-SHA256, compared in constant time) before they change your plan.

## Reporting a vulnerability

Found a security issue in any part of OpenEng: an app, an engine, the Console, the Web Console, a
catalog or one of the websites? Email **[hello@openeng.app](mailto:hello@openeng.app)** rather than
filing a public issue or posting it publicly. Include:

- what's affected, with the version (`openeng-terminal --version` prints an app engine's version; the
  apps show it in the account menu);
- the steps to reproduce it;
- what an attacker could do with it.

Please give us the chance to fix it before you disclose it.

## What we'd like you to do

- Treat `OPENENG_KEY` like a password: one key per machine-job, revoke on
  retirement.
- Keep write-capable cells in separate deployments from read cells.
- Give a connector a write key only where writes are needed, and connect database connectors with a
  read-only database role ([Connectors](connectors.md)).
- Set a [token limit](govern.md) — the best guardrail for unattended runs.
- Use `https://` everywhere the platform asks for a URL. The Console checks credentialed endpoints
  and plugin endpoints, but the API doesn't repeat those checks.
- Keep your engines current: when an app offers **Restart engine** for a downloaded update, take it.
