§ 05 · Build

Developer quickstart

Bring the full chain up locally from source — witness, query plane, gateway — and ask Claude Desktop a question against real witness data.

View as .md

This is the developer runbook for Tier 1 demo ship — bringing the full chain up locally and asking Claude Desktop a question against real witness data. For the operator-grade walkthrough (Docker only, no Rust toolchain), see the operator quickstart.

Prerequisites

  • macOS or Linux dev host
  • Docker + Docker Compose (for the witness)
  • Rust toolchain (rustup default stable, MSRV 1.85+)
  • mkcert (brew install mkcert on macOS) — only needed if you’re testing mTLS
  • Optional: Claude Desktop or another MCP-aware client

The chain

   You / Claude Desktop

            │ stdio MCP (or HTTP /mcp)

    Conversational Gateway   (off-box; this terminal)

            │ i3X v1 over HTTP or HTTPS+mTLS

        Query Plane          (on-box; Rust)

            │ HTTP to /i3x/v0/*

       Eris Witness          (on-box; Python in Docker)

            │ tshark capture + Postgres + Redis

      Network / PCAP fixtures

Step 1 — Bring up the witness

The Python witness lives at ~/eriswitness/ and is symlinked at services/witness/. It’s its own multi-service Docker Compose stack (Flask app + 15 workers + Postgres + Redis).

macOS without Docker Desktop? Colima provides the Docker socket. Install with brew install colima docker docker-compose, start with colima start, then export DOCKER_HOST=unix://$HOME/.colima/default/docker.sock so docker compose finds it.

1a. Populate .env

The witness reads required secrets from services/witness/.env. Five are NOT NULL and must be set before the first boot:

cd services/witness
[ -f .env ] || cp .env.example .env

cat >>.env <<'EOF'
DB_PASSWORD=devpassword
SECRET_KEY=dev-secret-not-for-prod
ADMIN_PASSWORD=admin-dev-password
ARTIFACT_ENCRYPTION_KEY=dev-artifact-key-32-bytes-long!
SMTP_FROM=witness@localhost
EOF

For a real deployment, generate strong values and store them via your secrets manager — these defaults are dev-only.

1b. Build and run the stack

docker compose build              # ~10 min on first run (Cython compile)
docker compose up -d

1c. Run database migrations

The compose stack does not run migrations on first boot. The app will crashloop with FATAL: required tables missing until you do this once:

docker compose run --rm --entrypoint "alembic upgrade head" app
docker compose restart app

Confirm the stack is healthy:

docker compose ps
curl -k http://127.0.0.1:5001/

1d. Provision an org and i3X API key (headless)

docker exec eriswitness-dev-app python -c "
from app import create_app, db
from app.models import Organization
import secrets, hashlib

app = create_app()
with app.app_context():
    org = Organization.query.filter_by(name='default').first() or Organization(name='default')
    raw_key = 'cf-' + secrets.token_hex(16)
    org.i3x_api_key_hash = hashlib.sha256(raw_key.encode()).hexdigest()
    db.session.add(org); db.session.commit()
    print(f'ORG_ID={org.id}')
    print(f'I3X_API_KEY={raw_key}')
"

The I3X_API_KEY line is your WITNESS_API_KEY for Step 2 — copy it now, the hash is one-way.

1e. Load sample data

Option A — load a PCAP fixture (recommended for realistic data): in the web UI go to New Scan → Upload PCAP and select a file from services/witness/presets/ics-ot/.

Option B — seed a few assets directly (for smoke testing):

docker exec eriswitness-dev-app python -c "
from app import create_app, db
from app.models import Asset
from datetime import datetime, timezone

app = create_app()
with app.app_context():
    now = datetime.now(timezone.utc)
    for ip, mac, vendor, role in [
        ('10.5.0.10', '00:1c:06:aa:bb:01', 'Allen-Bradley', 'PLC'),
        ('10.5.0.11', '00:0e:8c:cc:dd:02', 'Siemens',       'HMI'),
        ('10.5.0.12', '00:80:f4:ee:ff:03', 'Schneider',     'Sensor'),
    ]:
        a = Asset(ip=ip, mac=mac, vendor=vendor, role=role,
                  first_seen=now, last_seen=now, organization_id=1)
        db.session.add(a)
    db.session.commit()
    print('seeded 3 assets')
"

Step 2 — Bring up the query plane

In a new terminal at the conversational-factory repo root:

WITNESS_URL=http://127.0.0.1:5001 \
  WITNESS_API_KEY="<paste-the-key-from-step-1>" \
  cargo run -p query-plane

Verify connectivity:

curl -s http://127.0.0.1:8090/v1/info | jq
# expect capabilities.query.history = true (witness wired)

Step 3 — Bring up the gateway

QUERY_PLANE_URL=http://127.0.0.1:8090 \
  AUDIT_LOG_PATH=/tmp/cf-audit.jsonl \
  cargo run -p conversational-gateway

Smoke check:

curl -s http://127.0.0.1:8091/health
curl -s http://127.0.0.1:8091/tools | jq '.tools | length'

Step 4 — Make a real query (HTTP path)

curl -s http://127.0.0.1:8090/v1/objects | jq '.result[].elementId' | head -10

ELEMENT_ID="<paste-an-elementId-from-above>"

curl -s -X POST http://127.0.0.1:8091/query \
  -H 'content-type: application/json' \
  -d "$(jq -nc --arg eid "$ELEMENT_ID" '{
    request_id: "00000000-0000-0000-0000-000000000001",
    intent: "get-current-state",
    elementId: $eid,
    maxDepth: 2
  }')" | jq

Inspect the audit chain:

tail -1 /tmp/cf-audit.jsonl | jq

Step 5 — Connect Claude Desktop (optional, MCP path)

Edit ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "factory": {
      "command": "/path/to/conversational-factory/target/release/conversational-gateway",
      "args": ["--stdio"],
      "env": {
        "QUERY_PLANE_URL": "http://127.0.0.1:8090",
        "AUDIT_LOG_PATH": "/tmp/cf-audit.jsonl"
      }
    }
  }
}

Build the gateway in release mode:

cargo build --release -p conversational-gateway

Restart Claude Desktop. Ask: “Use the factory MCP server. What objects exist on Cell 5? Get the current state of one of them.”

Step 6 — Verify the audit chain

cat /tmp/cf-audit.jsonl | jq -s 'group_by(.tool) | map({tool: .[0].tool, count: length})'

The audit log carries: the natural-language question, the exact tool dispatched, the parameters, the downstream i3X call(s), and the status returned to the AI.

Dev mode vs production

ConcernDev (this guide)Production
Query plane transportPlain HTTP loopbackmTLS with operator-issued certs
Gateway locationSame host as query planeOff-box on operator workstation / broker box
Witness API keyRead from logs / web UIProvisioned via signed-config artifact
Audit log path/tmp/cf-audit.jsonl/var/log/conversational-factory/gateway-audit.jsonl with rotation
Sample dataLoaded from presets/Real plant traffic

Troubleshooting

If something doesn’t behave, run make smoke-run. Common surprises:

  • docker compose up exits with app crashlooping — schema not migrated. Run the alembic upgrade head step from 1c.
  • docker compose up complains about missing required env — five secrets in 1a (DB_PASSWORD, SECRET_KEY, ADMIN_PASSWORD, ARTIFACT_ENCRYPTION_KEY, SMTP_FROM) are NOT NULL.
  • Query plane reports query.history: false — witness URL or API key is wrong; ping failed at startup.
  • get_current_state returns GoodNoData for everything — witness has no AssetDB rows yet; load a PCAP or run the seed snippet from 1e.
  • Asset insert IntegrityError: first_seen ... not-null — both first_seen and last_seen are required.
  • tools/call returns audit-failureAUDIT_LOG_PATH directory isn’t writable by the gateway process.
  • Gateway can’t reach query plane — port mismatch (default 8090) or one process is bound to localhost only.
  • Witness rejects the i3X bearer token — key was regenerated but env var still has the old hash.