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 .mdThis 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 mkcerton 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 withcolima start, then exportDOCKER_HOST=unix://$HOME/.colima/default/docker.socksodocker composefinds 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
| Concern | Dev (this guide) | Production |
|---|---|---|
| Query plane transport | Plain HTTP loopback | mTLS with operator-issued certs |
| Gateway location | Same host as query plane | Off-box on operator workstation / broker box |
| Witness API key | Read from logs / web UI | Provisioned via signed-config artifact |
| Audit log path | /tmp/cf-audit.jsonl | /var/log/conversational-factory/gateway-audit.jsonl with rotation |
| Sample data | Loaded from presets/ | Real plant traffic |
Troubleshooting
If something doesn’t behave, run make smoke-run. Common surprises:
docker compose upexits withappcrashlooping — schema not migrated. Run thealembic upgrade headstep from 1c.docker compose upcomplains 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_statereturnsGoodNoDatafor everything — witness has no AssetDB rows yet; load a PCAP or run the seed snippet from 1e.Asset insert IntegrityError: first_seen ... not-null— bothfirst_seenandlast_seenare required.tools/callreturnsaudit-failure—AUDIT_LOG_PATHdirectory 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.