Metadata-Version: 2.4
Name: ghostai-connect
Version: 0.2.14
Summary: GhostAI BYO-inference connector — outbound WSS tunnel from a customer-run Ollama/vLLM to ghostai-relay, no inbound firewall rule required
Author: Dymium, Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Changelog, https://github.com/dymium-io/ghostai-connect/releases
Keywords: ghostai,byo-inference,connector,relay
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: System Administrators
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.0
Requires-Dist: httpx>=0.26
Requires-Dist: websockets>=12.0
Requires-Dist: pydantic>=2.5.3
Requires-Dist: cryptography>=44.0.1
Provides-Extra: test
Requires-Dist: pytest>=8.0.0; extra == "test"
Requires-Dist: pytest-asyncio>=0.23.0; extra == "test"
Dynamic: license-file

## What's new in 0.2.14

- Update with ghostai-connect update: it asks, installs the version GhostAI named, and restarts the service only when you say so
- ghostai-connect update refuses inside the connector container; upgrade the image instead
- List the model calls this computer served with ghostai-connect calls: time, GhostAI user, model, token counts and engine
- The call list never stores the prompt or the answer

# GhostAI Connect

GhostAI Connect links an inference server you run yourself (Ollama, vLLM, or
any OpenAI-compatible server) to GhostAI. It opens one outbound WebSocket
connection to the GhostAI relay, so you never open an inbound firewall port.

## Install (recommended: hash-locked)

Each release from 0.2.7 attaches `ghostai-connect-<version>-install.lock` to
its GitHub release. It lists the sha256 of every file the connector installs:
each dependency, including PyCA `cryptography`, and the `ghostai-connect`
wheel itself. pip then refuses any file whose hash does not match, wherever it
came from. RELEASE_SIGNING.md shows how to verify the lock's signature first.

Python 3.10 or later:

```bash
python3 -m venv ~/.local/share/ghostai-connect
~/.local/share/ghostai-connect/bin/pip install --require-hashes --no-deps --only-binary :all: \
  -r ghostai-connect-<version>-install.lock
ln -sf ~/.local/share/ghostai-connect/bin/ghostai-connect ~/.local/bin/ghostai-connect
ghostai-connect --version
```

On Windows, create the environment with `py -m venv %LOCALAPPDATA%\ghostai-connect`
and run `%LOCALAPPDATA%\ghostai-connect\Scripts\pip` with the same arguments.

- `--require-hashes`: every file must match a hash in the lock.
- `--no-deps`: install exactly what the lock lists and resolve nothing else.
- `--only-binary :all:`: never build from source. A source build downloads its
  build tools without checking hashes. Every supported platform has wheels; if
  pip finds none for yours, contact support rather than dropping the flag.

To upgrade, delete `~/.local/share/ghostai-connect` and repeat these steps with
the new version's lock. Your enrollment and key are not in that directory, so
they are kept.

pipx cannot install with hash checking. It upgrades pip in its own shared
environment and installs the package name you give it, neither of which has a
hash, so `--pip-args="--require-hashes ..."` makes both steps fail. Use a
virtual environment as above for the hash-locked install; the other ways below
do not check hashes.

## Install

Python 3.10 or later:

```bash
pipx install ghostai-connect      # or: pip install ghostai-connect
ghostai-connect --version
```

Or use the container image for `linux/amd64` and `linux/arm64`:
`ghcr.io/dymium-io/ghostai-connect:<version>`.

## Verify before you run it

The container image is signed keyless with Sigstore by this repository's
release workflow. With [`cosign`](https://docs.sigstore.dev/cosign/system_config/installation/)
v2 or later:

```bash
cosign verify \
  --certificate-identity-regexp '^https://github\.com/dymium-io/ghostai-connect/\.github/workflows/publish-image\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  ghcr.io/dymium-io/ghostai-connect:<version>
```

The PyPI files carry [PEP 740](https://peps.python.org/pep-0740/) attestations
from the same repository. With `pipx install pypi-attestations`:

```bash
pypi-attestations verify pypi \
  --repository https://github.com/dymium-io/ghostai-connect \
  pypi:ghostai_connect-<version>-py3-none-any.whl
```

RELEASE_SIGNING.md in the source repository has the details.

## Enroll and run

Use the enrollment command shown in the GhostAI admin console, and paste the
token when it asks. The token is single-use, and it is read with the input
hidden, so it stays out of your shell history and the process list.

```bash
ghostai-connect enroll \
  --base-url https://your-ghostai-instance.example.com \
  --relay-url wss://relay-connector.your-ghostai-instance.example.com/connector/ws

ghostai-connect run      # runs until stopped; reconnects automatically
ghostai-connect doctor   # last heartbeat, registration status, last error
```

Without a terminal, pipe the token in with `--token-file -`, read it from a
file only you can read with `--token-file PATH` (it warns when other users can
read the file), or set `GHOSTAI_CONNECT_ENROLL_TOKEN` (least preferred: the
environment of a process is visible to the same user and to `docker inspect`).
These, and the prompt, need 0.2.9 or later. On 0.2.8 or earlier, put the token
last, after `--`: `ghostai-connect enroll --base-url ... --relay-url ... -- <token>`.
A token can begin with `-`, and without `--` it is read as an option.

Both URLs must be `https://` and `wss://`; `http://` and `ws://` are accepted
only for `localhost`, or with `--allow-insecure` for local development.

The enrolled credential and the call-signing key are kept in your operating
system's credential store: the macOS login keychain, the Linux Secret Service
(GNOME Keyring or KWallet, through `secret-tool`), or Windows DPAPI. Where no
store is usable (a server without a desktop session, and without
`tpm2-tools`) they stay in `~/.config/ghostai-connect/config.json` with mode
`0600`, and `enroll` and `run` say so. On Linux, installing `tpm2-tools` or
`secret-tool` uses a sealed store instead; neither is required to enroll.
Set `GHOSTAI_CONNECT_KEYSTORE` to `keychain`, `sealed` or `file` to choose
explicitly. Do not restore this key from a backup or copy it
to another computer: GhostAI treats an older copy as stolen and revokes the
connector. Run one `ghostai-connect run` per enrollment.

| Environment variable | Meaning | Default |
|---|---|---|
| `GHOSTAI_CONNECT_LOCAL_URL` | Your local inference server | `http://localhost:11434` |
| `GHOSTAI_CONNECT_MAX_CONCURRENCY` | Concurrent requests forwarded to it | `4` |
| `GHOSTAI_CONNECT_MAX_RECONNECT_ATTEMPTS` | Give up after this many consecutive failed reconnects | retry forever |
| `GHOSTAI_CONNECT_KEYSTORE` | Where the key is kept: `auto`, `keychain`, `sealed` or `file` | `auto` (`sealed` in the image) |

`ghostai-connect run` first checks that it can save its state, and refuses to
start if it cannot: every connection advances a counter it must save.

### Interactive mode

Run `ghostai-connect` with no arguments in a terminal (or
`ghostai-connect interactive`) for a numbered menu covering enroll, run,
status, and local MCP targets. Each item runs the same command with the same
checks. The enrollment token is read without echo, and removing or replacing
something asks you to type a word to confirm. Set `NO_COLOR` to turn off
color. The menu opens only when both input and output are a terminal: in a
container, a service unit, a script, or CI, `ghostai-connect` with no
arguments prints its usage as before.

### Run at login or boot

```bash
ghostai-connect service install      # start it now, at every login, and after it stops
ghostai-connect logs -n 100 -f       # its recent output
ghostai-connect service uninstall    # stop and remove it; the enrollment is kept
```

On macOS this is a launchd user agent
(`~/Library/LaunchAgents/com.dymium.ghostai-connect.plist`, output in
`~/Library/Logs/ghostai-connect/connect.log`). On Linux it is a systemd user
unit (`~/.config/systemd/user/ghostai-connect.service`, output in journald);
to start it at boot without logging in, turn on lingering with
`loginctl enable-linger <you>`, which `service install` reminds you of. The
service runs the `ghostai-connect` you installed it with, plus `run`, and
holds no token or key. When the connector was revoked or its key store refuses
its keys, `run` exits with code 78 and the systemd unit does not restart it.
Not available in a container (use `--restart unless-stopped`) or on Windows.

## Install Ollama or vLLM

```bash
ghostai-connect engine install ollama --model llama3.2    # asks, then installs and pulls
ghostai-connect engine install vllm --model org/name       # Linux + Docker; names the model on the start command
```

Ollama uses Homebrew on macOS (and starts the service before a model pull), `https://ollama.com/install.sh` on Linux including a Jetson, and winget on Windows. It listens on `http://localhost:11434`. vLLM is `docker pull vllm/vllm-openai` on Linux and listens on `http://localhost:8000` after you start a model. On a Jetson, macOS, and Windows, install Ollama instead. Download progress and a password prompt stay on the terminal. Add `--yes` to skip the confirmation. A command already on `PATH` is left as it is, and an Ollama `--model` is still pulled.

llama.cpp, LM Studio, and a generic OpenAI-compatible server have no installer in this package. Start the server you already run, then enroll with `--local-inference-url`. The usual addresses are `http://localhost:8080` (llama.cpp) and `http://localhost:1234` (LM Studio). An OpenAI-compatible server has no default port. `engine install llama-cpp` (or `lmstudio`, or `openai-compatible`) says so and does not download a binary. A public URL is refused unless you pass `--allow-public-inference-url`.

## Point a desktop app at GhostAI

From 0.2.10, `ghostai-connect apps connect <app>` writes a desktop app's own
config so the app sends its model requests, and its calls to GhostAI's hosted
MCP tools at `<base-url>/mcp`, to GhostAI. It does not start the relay tunnel
and does not need an enrollment.

```bash
read -rs GHOSTAI_API_KEY && export GHOSTAI_API_KEY     # paste the gst_ key; it is not echoed
ghostai-connect apps connect claude-code --base-url https://your-ghostai-instance.example.com
ghostai-connect apps restore claude-code               # later: put it back
```

| App | What it writes |
|---|---|
| `claude-desktop` | A GhostAI gateway profile (base URL, API key, model list) under `Claude-3p/configLibrary/`, and a `ghostai` MCP server in `claude_desktop_config.json`, started as `npx -y mcp-remote` (needs Node.js) |
| `codex` (ChatGPT Codex) | `model`, `openai_base_url`, `model_catalog_json` and `[mcp_servers.ghostai]` in `~/.codex/config.toml`, `~/.codex/ghostai-codex-models.json`, and the key in `~/.codex/auth.json` |
| `claude-code` | `ANTHROPIC_BASE_URL` and `ANTHROPIC_API_KEY` in `~/.claude/settings.json`, and a `ghostai` MCP server in `~/.claude.json` |
| `cursor` | The OpenAI base URL, the key and the model list in Cursor's settings database, and a `ghostai` MCP server in `~/.cursor/mcp.json`. Chat and Agent only; Tab autocomplete stays on Cursor's models |

Without `--model`, the model list is what `GET /v1/models` returns for the key,
plus the models on the local server the connector carries. On macOS,
`--restart` quits and reopens the app. The files it replaces are saved first
under `~/.local/state/ghostai-desktop/<app>/`, and `apps restore <app>` puts
them back. If the app already points at Ollama's own loopback switch, it
writes nothing and exits with code 2.

- **The API key is written in plaintext** (mode `0600`) into those apps' config
  files, except Cursor's settings database, where Cursor's own encryption seals
  it. Claude Desktop also passes it on the command line of its `mcp-remote`
  helper, so anyone on that computer who can list processes can read it while
  Claude Desktop runs.
- **Use a narrowly scoped key**: a separate `gst_` key per app with only the
  scopes it needs (usually `chat` and `read`), never `admin`, revoked when you
  stop using the app. Pass it in `GHOSTAI_API_KEY` rather than `--api-key`,
  which lands in your shell history.
- **Verified on macOS only.** On Linux and Windows it writes the same settings
  at those platforms' paths, but it has not been run against the real apps
  there yet.
- **Where the data goes.** The app sends each prompt to GhostAI's cloud, where
  it is scanned for PII before it reaches a model. A model served by your
  connector runs on hardware you choose, and the relay sees only encrypted
  traffic. Local MCP targets you added with `ghostai-connect mcp add` stay in
  the connector's config on this computer.

## Running in Docker / Kubernetes

The image `ghcr.io/dymium-io/ghostai-connect:<version>` runs `ghostai-connect run`
as user `1000`. A container has no keychain, so the image seals the
connector's keys (AES-256-GCM) under a **key-encryption key (KEK)** you give
it, and keeps the sealed keys and the rest of its state on a volume. It needs
two things:

- **A writable state volume** at `/home/connect/.config/ghostai-connect`, one
  per running connector. Never share one between two containers, start a
  second container from a copy, or restore one from a backup: the state
  advances with every connection, and GhostAI quarantines a connector that
  comes back with an older copy until an admin approves the restore.
- **A KEK**: 32 random bytes, base64, mounted read-only as a secret at
  `/run/secrets/ghostai-connect-kek`, or wrapped with AWS KMS (below). Keep it
  out of the volume and out of the image: the volume alone is then useless to
  whoever copies it.

The connector refuses to enroll (before it uses the token) or to start when
its keys would be in plaintext, when its state is in the container's writable
layer instead of a volume, when the KEK file is in the writable layer or in
the state volume, or when the state cannot be written.

| Environment variable | Meaning |
|---|---|
| `GHOSTAI_CONNECT_KEK_FILE` | The KEK secret file (default `/run/secrets/ghostai-connect-kek`) |
| `GHOSTAI_CONNECT_KEK_KMS_CIPHERTEXT_FILE` | The KEK as AWS KMS ciphertext, instead of a KEK file |
| `GHOSTAI_CONNECT_KEK_KMS_KEY_ID` | The KMS key ARN; KMS refuses ciphertext made under any other key |
| `GHOSTAI_CONNECT_KEK_KMS_REGION` | The KMS key's region (default `AWS_REGION`) |
| `GHOSTAI_CONNECT_ALLOW_PLAINTEXT_SECRETS` | `1` keeps the keys in plaintext anyway, with a warning. Throwaway tests only |

The KEK is never read from an environment variable: `docker inspect` shows
those.

### Docker

```bash
# Once. Store this file like a password, outside the volume.
head -c 32 /dev/urandom | base64 > ghostai-connect-kek
sudo chown 1000 ghostai-connect-kek && chmod 400 ghostai-connect-kek

docker run --rm \
  -v ghostai-connect:/home/connect/.config/ghostai-connect \
  -v "$PWD/ghostai-connect-kek:/run/secrets/ghostai-connect-kek:ro" \
  ghcr.io/dymium-io/ghostai-connect:<version> \
  enroll --base-url ... --relay-url ... -- <token>

docker run -d --name ghostai-connect --restart unless-stopped --read-only \
  -v ghostai-connect:/home/connect/.config/ghostai-connect \
  -v "$PWD/ghostai-connect-kek:/run/secrets/ghostai-connect-kek:ro" \
  -e GHOSTAI_CONNECT_LOCAL_URL=http://ollama:11434 \
  ghcr.io/dymium-io/ghostai-connect:<version>
```

`--read-only` is optional; it leaves the connector no writable layer at all.

### Docker Compose

```yaml
services:
  ghostai-connect:
    image: ghcr.io/dymium-io/ghostai-connect:<version>
    restart: unless-stopped
    read_only: true
    environment:
      GHOSTAI_CONNECT_LOCAL_URL: http://ollama:11434
    volumes:
      - ghostai-connect:/home/connect/.config/ghostai-connect
    secrets:
      - ghostai-connect-kek   # mounted at /run/secrets/ghostai-connect-kek
secrets:
  ghostai-connect-kek:
    file: ./ghostai-connect-kek
volumes:
  ghostai-connect:
```

Enroll once with `docker compose run --rm ghostai-connect enroll --base-url ... --relay-url ... -- <token>`,
then `docker compose up -d`. From 0.2.9, leave off `-- <token>` and
`docker compose run` asks for it, hidden.

### Kubernetes

The KEK is a Secret, mounted read-only (Kubernetes keeps Secret volumes on
tmpfs). Mount it outside `/run/secrets`, where it would hide the service
account token. The state is a PersistentVolumeClaim. Use one replica with the
`Recreate` strategy, so two pods never run at once.

```bash
kubectl create secret generic ghostai-connect-kek \
  --from-literal=ghostai-connect-kek="$(head -c 32 /dev/urandom | base64)"
```

```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: ghostai-connect-state}
spec:
  accessModes: [ReadWriteOnce]
  resources: {requests: {storage: 16Mi}}
---
apiVersion: apps/v1
kind: Deployment
metadata: {name: ghostai-connect}
spec:
  replicas: 1
  strategy: {type: Recreate}
  selector: {matchLabels: {app: ghostai-connect}}
  template:
    metadata: {labels: {app: ghostai-connect}}
    spec:
      securityContext: {runAsUser: 1000, runAsGroup: 1000, fsGroup: 1000}
      containers:
        - name: connect
          image: ghcr.io/dymium-io/ghostai-connect:<version>
          args: ["run"]
          env:
            - {name: GHOSTAI_CONNECT_LOCAL_URL, value: "http://ollama:11434"}
            - {name: GHOSTAI_CONNECT_KEK_FILE, value: /etc/ghostai-connect-kek/ghostai-connect-kek}
          securityContext: {readOnlyRootFilesystem: true, allowPrivilegeEscalation: false}
          volumeMounts:
            - {name: state, mountPath: /home/connect/.config/ghostai-connect}
            - {name: kek, mountPath: /etc/ghostai-connect-kek, readOnly: true}
      volumes:
        - name: state
          persistentVolumeClaim: {claimName: ghostai-connect-state}
        - name: kek
          secret: {secretName: ghostai-connect-kek, defaultMode: 0400}
```

To enroll, run the same pod spec once with
`args: ["enroll", "--base-url", "...", "--relay-url", "...", "--", "<token>"]` (for
example as a Job, with the Deployment scaled to 0), then scale the Deployment
to 1. From 0.2.9 you can keep the token out of the pod spec: put it in a Secret,
drop `"--", "<token>"`, and set `GHOSTAI_CONNECT_ENROLL_TOKEN` from the Secret
with `valueFrom.secretKeyRef`. Delete the Secret once enrolled.

### AWS KMS with a workload identity (EKS IRSA, ECS, EC2)

Instead of a KEK secret, keep only KMS ciphertext of the KEK beside the
connector. The workload's own AWS identity decrypts it at startup, so the
plaintext KEK is never stored anywhere, a copy of the volume and the
ciphertext is useless without that identity, and every use is in CloudTrail.

```bash
aws kms encrypt --key-id alias/ghostai-connect \
  --plaintext fileb://<(head -c 32 /dev/urandom) \
  --encryption-context ghostai-connect=kek \
  --query CiphertextBlob --output text > ghostai-connect-kek.kms
kubectl create configmap ghostai-connect-kek-kms --from-file=ghostai-connect-kek.kms
```

Give the pod's IAM role (IRSA) `kms:Decrypt` on that key, with the condition
`"kms:EncryptionContext:ghostai-connect": "kek"`. In the Deployment above,
use that service account, mount the ConfigMap at `/etc/ghostai-connect-kek`
instead of the Secret, and replace `GHOSTAI_CONNECT_KEK_FILE` with:

```yaml
          env:
            - {name: GHOSTAI_CONNECT_KEK_KMS_CIPHERTEXT_FILE, value: /etc/ghostai-connect-kek/ghostai-connect-kek.kms}
            - {name: GHOSTAI_CONNECT_KEK_KMS_KEY_ID, value: "arn:aws:kms:<region>:<account>:key/<key-id>"}
            - {name: GHOSTAI_CONNECT_KEK_KMS_REGION, value: "<region>"}
```

The connector uses, in order: `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`, a
web identity token (IRSA), the container credentials endpoint (ECS task
roles, EKS Pod Identity), then EC2 instance metadata (IMDSv2 only). It does
not read `~/.aws`. `AWS_ENDPOINT_URL_KMS` selects a VPC or FIPS endpoint.
For other clouds, deliver the KEK as a file from your secret manager (for
example its Kubernetes CSI driver) and use `GHOSTAI_CONNECT_KEK_FILE`.

### TPM passthrough

On a Linux host with a TPM 2.0, the connector can keep its device identity key
in the TPM, where it cannot be copied at all (see the keys section above).
The published image does not include `tpm2-tools`, so that it stays small and
reproducible; build a derived image and pass the TPM resource manager in:

```dockerfile
FROM ghcr.io/dymium-io/ghostai-connect:<version>
USER root
RUN apt-get update && apt-get install -y --no-install-recommends tpm2-tools \
    && rm -rf /var/lib/apt/lists/*
USER connect:connect
```

```bash
docker run ... --device /dev/tpmrm0 --group-add "$(stat -c %g /dev/tpmrm0)" <your-image>
```

Enroll with the TPM present. The state volume then holds only a reference to
the TPM key, so a copy of the volume and the KEK still cannot act as the
connector on another machine. Keep the KEK as well: the call keys and the
connection counter are still sealed under it.

### Upgrading a 0.2.6 container

0.2.6 kept the keys in plaintext in the volume, and its enrollment does not
carry over to 0.2.7. Enroll 0.2.7 into a new volume with a KEK, then delete the
old volume. (Started on a plaintext volume with a KEK, 0.2.7 seals the keys in
place, but the old plaintext can linger on the volume's disk.)

## Laptops and sleep

Close the lid whenever you like. From 0.2.7 the connector notices a wake or a
network change within a second, drops the dead connection and connects again
within a second or two of the network being back, retrying every second while
Wi-Fi rejoins. While the computer sleeps, anything that uses the connector
fails at once with *connector offline* rather than waiting for a timeout.

If the credential store is locked after a wake (a keychain set to lock on
sleep, or a Linux keyring that locks on suspend), the connector waits and
`ghostai-connect doctor` shows `waiting for keystore unlock`; it never moves
the key somewhere less protected. Other statuses you may see between
connections: `reconnecting after sleep`, `reconnecting after a network change`
and `waiting for network`.

## Upgrade

```bash
pipx upgrade ghostai-connect      # or: pip install --upgrade ghostai-connect
```

Your enrollment is kept across upgrades from 0.2.7 on. Restart
`ghostai-connect run` after upgrading. Upgrading from 0.2.6 or earlier needs a
new enrollment, because 0.2.7 binds every connection to a device key created
at enrollment.

GhostAI accepts 0.2.9 and later only. 0.2.7 and 0.2.8 must upgrade. An enrollment from 0.2.9 or later does not re-enroll.
0.2.14 speaks relay protocol 7 only. It does not step down to protocol 6.
Everything between GhostAI and the
connector is encrypted end to end with fresh keys for every connection, so the
relay sees only which connector a message is for and how large it is. Every
call is also signed with a key issued at enrollment, and the connector refuses
anything that does not decrypt or check out; GhostAI does the same with every
reply. If two computers use the same key, or an old copy of it connects,
GhostAI revokes the connector and alerts your admins. The key can be rotated
without enrolling again. Versions before 0.2.4 have no key: enroll again with
a new token after upgrading.

## License

Proprietary. Copyright (c) 2026 Dymium, Inc. All rights reserved. Use requires
an active GhostAI subscription, under your agreement with Dymium, Inc.; see the
LICENSE file included with the package. Questions: support@dymium.io.
