Metadata-Version: 2.4
Name: ghostai-connect
Version: 0.2.7
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
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

# 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. The token is
single-use.

```bash
ghostai-connect enroll <token> \
  --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
```

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) they stay in
`~/.config/ghostai-connect/config.json` with mode `0600`, and
`enroll` and `run` say so. Set `GHOSTAI_CONNECT_KEYSTORE` to `keychain` 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.

## 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 <token> --base-url ... --relay-url ...

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 <token> --base-url ... --relay-url ...`,
then `docker compose up -d`.

### 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", "<token>", "--base-url", "...", "--relay-url", "..."]` (for
example as a Job, with the Deployment scaled to 0), then scale the Deployment
to 1.

### 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.4 on. Restart
`ghostai-connect run` after upgrading; on its first start 0.2.6 moves an
existing key out of `config.json` into your credential store, where there is
one.

GhostAI accepts 0.2.6 and later only. 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.
