🧪 Laya + SOC — Demo: setup → install → configure → validate
2026-09-27 · end-to-end walkthrough of the deployed pattern (laya 0.3.20) · every expected output below is real, measured on this stack
0 · What you'll build
Topology wazuh alerts / email ~480 alerts/day realtime_ingest :8765 + imap-watcher (edge host) laya-serve :8099 CPU · 3 ckpts · loopback laya-shadow.jsonl log-only · LLM wins SOC routing unchanged in shadow log-only LAYA_MODE env flag off | shadow | gated controls the hook What this demo builds: shadow-mode Laya beside the live SOC — gemma path untouched
A decision sidecar beside the live SOC: gemma keeps deciding (shadow mode), Laya answers the same
questions and logs everything to laya-shadow.jsonl. After the soak gate passes, a one-line env flip
moves decisions to Laya with confidence-gated fallback. Total footprint: 4.7 GB RAM on each host, zero GPU.
1 · Setup — what you need before touching anything
Item Value used here Why
Host 1 (serve + control) control host (16 logical cores, 109 GB RAM) repo, evals, source of truth
Host 2 (decision traffic) edge host (runs wazuh + ingest + mail) Laya must sit next to its callers (loopback-only)
Python ≥3.10 (3.12.3 used) venv per host
Ports 8099 (laya-serve, loopback) · 8765 (ingest, edge host) 8000 default avoided on purpose
Disk ~5 GB venv + 2.3 GB checkpoints CPU torch wheel saves ~5 GB
Network HF reachable once (preload), offline after HF_HUB_OFFLINE=1 at runtime
Decision to make up front: which questions each part answers (the question set) and where its truth comes from
(deterministic rule vs LLM verdict). The SOC contract lives in soc-agents/soc-triage/IDENTITY.md. If you can't state
the decision contract in one sentence, don't wire the hook yet.
2 · Install (control host first, then replicate)
2.1 venv + CPU torch + laya — order matters
python3 -m venv ~/laya/.venv
~/laya/.venv/bin/python -m pip install --upgrade pip
# CPU-only torch FIRST (PyPI default pulls multi-GB CUDA):
~/laya/.venv/bin/python -m pip install torch --index-url https://download.pytorch.org/whl/cpu
~/laya/.venv/bin/python -m pip install "laya[serve]"
# verify
~/laya/.venv/bin/python -I -c "import laya; print(laya.__version__)" # → 0.3.20
2.2 Preload checkpoints (needs HF network this once)
export HF_HOME=~/laya/hf-cache
~/laya/.venv/bin/python -c "from laya import Router; Router(preload=True)"
# → all 3 checkpoints (english, multilingual, typed-decisions), 2.3 GB total
2.3 systemd user unit
# ~/.config/systemd/user/laya-serve.service (full text: laya-control/systemd/)
systemctl --user daemon-reload
systemctl --user enable --now laya-serve.service
journalctl --user -u laya-serve -f # expect: "3 models loaded" then uvicorn on 127.0.0.1:8099
2.4 Replicate to edge host (proven cross-host rsync)
rsync -a ~/laya edge host:~/laya # ~3.5 GB incl venv — same abs paths, python 3.12 both
scp ~/.config/systemd/user/laya-serve.service edge host:~/.config/systemd/user/
ssh edge host 'sed -i "s/LAYA_THREADS=8/LAYA_THREADS=6/" ~/.config/systemd/user/laya-serve.service' # edge host has 6 usable threads
ssh edge host 'systemctl --user daemon-reload && systemctl --user enable --now laya-serve && loginctl enable-linger wez'
linger matters — user units die at logout without it. edge host system services need sudo; laya-serve is a user unit (mixed model, that's fine).
3 · Configure
3.1 laya-serve (env-vars only — no CLI flags exist)
Env Value Note
LAYA_HOST / LAYA_PORT 127.0.0.1 / 8099 loopback-only; set LAYA_API_KEY before ever exposing
LAYA_DEVICE cpu
LAYA_PRELOAD 1 all 3 ckpts at boot, 4.7 GB RAM
LAYA_THREADS 8 (control host) / 6 (edge host) ≤ physical cores, oversubscription regresses hard
LAYA_AUTO_TASK 0 explicit routing, not auto
HF_HOME / HF_HUB_OFFLINE ~/laya/hf-cache / 1 offline works only after preload (§2.2)
3.2 Flip the SOC services into shadow mode
# edge host — one line each, created if absent (that's why the first test logged nothing!)
sudo -n tee /etc/default/realtime-soc-server <<'EOF'
# Laya shadow mode (Phase 1): off|shadow|gated. shadow = log-only, LLM verdict still wins.
LAYA_MODE=shadow
EOF
sudo -n tee /etc/default/imap-watcher <<'EOF'
# Laya shadow mode (Phase 1): off|shadow|gated. shadow = log-only, mail routing unchanged.
LAYA_MODE=shadow
EOF
# restart the system units (sudo) and verify
sudo -n systemctl restart realtime-soc-server imap-watcher
ssh edge host 'grep LAYA /etc/default/realtime-soc-server /etc/default/imap-watcher'
3.3 The hooks (code side — deploy once, they're in prod)
# shadow client: /opt/soc-openclaw/lib/laya_shadow.py (stdlib-only, never raises)
# hook 1: realtime_ingest.ingest_wazuh_alert → 4 triage questions (v1.1: 3 + deterministic severity)
# hook 2: imap-watcher._process_one → 3 email questions
# backups before every patch: file.bak-laya-YYYYMMDD · py_compile after every edit · RESTART then verify
Rule learned twice: patch → compile → restart → verify . An E2E test against a stale process looks like a failure that isn't one.
3.4 What the deployed wiring looks like
Topology wazuh alerts / email ~480 alerts/day realtime_ingest :8765 + imap-watcher (edge host) laya-serve :8099 CPU · 3 ckpts · loopback laya-shadow.jsonl log-only · LLM wins SOC routing unchanged in shadow log-only LAYA_MODE env flag off | shadow | gated controls the hook What this demo builds: shadow-mode Laya beside the live SOC — gemma path untouched
4 · Validate — the ladder
Validation flow V1 /health both hosts V2 predict smoke curl + 0.5 s V3 ingest E2E shadow row V4 count grows shadow_count V5 agreement cron 05:55 V6 watchdog probes ok Validation ladder — each step has a concrete expected output; stop and fix if it deviates V7 = gate criteria (≥300 unique, agreement ≥ threshold) · V8 = rollback drill (LAYA_MODE=off)
V1 — health, both hosts
curl -s http://127.0.0.1:8099/health
{"status":"ok","loaded":["english","multilingual","typed-decisions"],"device":"cpu"}
V2 — direct predict (the smoke)
curl -s http://127.0.0.1:8099/v1/systemone -H 'content-type: application/json' -d '{"state":
"Alert: repeated failed SSH logins from 10.0.0.5 targeting prod-01 root over 3 minutes.
fail2ban banned the source IP. No successful logins observed.",
"questions": {"alert_class": {"type":"choice","instructions":"What kind of event is this?",
"criteria":{"bruteforce":"repeated auth failures","malware":"malicious code executed",
"benign":"routine noise","other":"anything else"}},
"urgency": {"type":"score","criteria":["not urgent","soon","critical"]},
"auto_close": {"type":"noul","instructions":"Safe to auto-close?"}}}'
# expected: full answers object, routing.model set, usage.input_tokens ≤ 512
# latency: ~0.5 s round-trip for 3 questions (measured 0.49 s)
V3 — end-to-end through the real ingest (edge host)
curl -s -X POST http://127.0.0.1:8765/ingest -H 'content-type: application/json' -d '{
"rule":{"id":5710,"level":12,"description":"Multiple authentication failures."},
"agent":{"name":"web-01"},
"data":{"srcip":"1.2.3.4"},
"decoder":{"name":"sshd"},"location":"/var/log/auth.log"}'
# expected (Option B behavior):
level parsed nested → severity high · NO gemma turn · ingest ≈1.65 s
# then confirm the shadow row:
ssh edge host 'tail -1 /opt/soc-openclaw/data/laya-shadow.jsonl | python3 -m json.tool'
# expect: answers.severity/response/escalate/known_pattern + latency_ms ~900 + context.level:12
V4 — shadow data grows
ssh edge host 'python3 /opt/soc-openclaw/lib/shadow_count.py'
# counts real (non-SELFTEST) decisions → was 3 → 561 raw / 452 unique over ~24 h organic traffic
V5 — agreement job (cron 05:55 UTC, run manually first)
ssh edge host 'python3 /opt/soc-openclaw/lib/laya_agreement.py | tail -3'
# expected: escalate_vs_ingest agreement 0.946 (n=443+) lat_mean ~1100 ms
# output file: /opt/soc-openclaw/data/laya-agreement-latest.json
V6 — watchdog
# soc-llm-watchdog cron (10 min) probes laya-serve on BOTH hosts + shadow-log staleness (>6h = BROKEN)
# expect: UNCHANGED (healthy) — and a BROKEN page if you kill laya-serve (that's the alarm working)
V7 — gate criteria (before flipping LAYA_MODE=gated)
Criterion Threshold Measured 2026-09-27
Unique shadow decisions ≥300 452 ✓
escalate agreement vs ingest rule ≥0.90 0.946 live · 0.979 nightly
known_pattern confidence band stable 0.68–0.74, p10–p90
response question gateable conf p50 0.04 → stays shadowed until finetune
V8 — rollback drill (do it once before you trust it)
ssh edge host 'sudo -n sed -i s/LAYA_MODE=shadow/LAYA_MODE=off/ /etc/default/realtime-soc-server'
ssh edge host 'sudo -n systemctl restart realtime-soc-server'
# POST an alert → ingest works, NO new shadow row, routing unchanged (deterministic sev + incidents)
# then restore: LAYA_MODE=shadow (or gated) + restart + tail the shadow log to confirm resumed
5 · The 2-minute demo script (show anyone)
# 1. it's alive: curl -s http://127.0.0.1:8099/health | jq .status # "ok"
# 2. it decides fast: time curl -s …/v1/systemone -d @demo-alert.json # ~0.5-1.1 s
# 3. it's wired in: POST :8765/ingest demo-alert.json → tail -1 laya-shadow.jsonl
# 4. gemma is untouched: grep -c gemma ~/logs/realtime-soc-server.log | tail -1 # no new turns
# 5. the numbers: cat /opt/soc-openclaw/data/laya-agreement-latest.json # 0.946
6 · Troubleshooting (the hits, ranked by pain)
Symptom Cause Fix
Shadow log stays empty after a test /etc/default/<svc> missing or not readCreate the file, restart the unit , verify with systemctl show -p Environment
E2E "fails" but code is right Forgot the restart after patch patch → py_compile → restart → verify (always in that order)
laya-serve won't start after adding a ckpt HF_HUB_OFFLINE=1 + not preloadedPreload manually first (§2.2), then restart
Inference suddenly slow Threads oversubscribed / concurrent pollers on single worker LAYA_THREADS ≤ physical cores; queue is by design
laya-serve --help "hangs" with 5 GB RAMIt's not --help — it booted the server Ctrl-C, configure via env vars in the unit
Confidence looks uncalibrated for some choice choice:11+ bin ships out-of-range temp (0.5)Expected until Phase 4 calibration re-fit
7 · Files this demo touches (complete map)
File What
~/laya/venv + HF cache (both hosts, rsync-cloned)
~/.config/systemd/user/laya-serve.servicethe service (both hosts; mirrored in laya-control/systemd/)
/etc/default/realtime-soc-server + /etc/default/imap-watcherLAYA_MODE switches (edge host)
/opt/soc-openclaw/lib/laya_shadow.pyshadow client + question sets (edge host)
/opt/soc-openclaw/data/laya-shadow.jsonlthe decision log (gitignored, backed up to GitLab data snapshot)
~/laya-control/evals/question-set YAML, baselines, agreement + tracker scripts
Companions: LAYA-USAGE-GUIDE.html (how to call it) · REPORT-LAYA-300-POWER-20260927.html (why: numbers) · LAYA-SETUP.md (deep install detail). Sources: all steps above executed and verified 2026-09-26/27; expected outputs are captured real values.