BEDIM SECURITY

🧪 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

Topologywazuh alerts / email~480 alerts/dayrealtime_ingest :8765+ imap-watcher (edge host)laya-serve :8099CPU · 3 ckpts · loopbacklaya-shadow.jsonllog-only · LLM winsSOC routingunchanged in shadowlog-onlyLAYA_MODE env flagoff | shadow | gatedcontrols the hookWhat 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

ItemValue used hereWhy
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
Ports8099 (laya-serve, loopback) · 8765 (ingest, edge host)8000 default avoided on purpose
Disk~5 GB venv + 2.3 GB checkpointsCPU torch wheel saves ~5 GB
NetworkHF reachable once (preload), offline afterHF_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)

EnvValueNote
LAYA_HOST / LAYA_PORT127.0.0.1 / 8099loopback-only; set LAYA_API_KEY before ever exposing
LAYA_DEVICEcpu
LAYA_PRELOAD1all 3 ckpts at boot, 4.7 GB RAM
LAYA_THREADS8 (control host) / 6 (edge host)≤ physical cores, oversubscription regresses hard
LAYA_AUTO_TASK0explicit routing, not auto
HF_HOME / HF_HUB_OFFLINE~/laya/hf-cache / 1offline 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

Topologywazuh alerts / email~480 alerts/dayrealtime_ingest :8765+ imap-watcher (edge host)laya-serve :8099CPU · 3 ckpts · loopbacklaya-shadow.jsonllog-only · LLM winsSOC routingunchanged in shadowlog-onlyLAYA_MODE env flagoff | shadow | gatedcontrols the hookWhat this demo builds: shadow-mode Laya beside the live SOC — gemma path untouched

4 · Validate — the ladder

Validation flowV1/healthboth hostsV2predict smokecurl + 0.5 sV3ingest E2Eshadow rowV4count growsshadow_countV5agreementcron 05:55V6watchdogprobes okValidation ladder — each step has a concrete expected output; stop and fix if it deviatesV7 = 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)

CriterionThresholdMeasured 2026-09-27
Unique shadow decisions≥300452 ✓
escalate agreement vs ingest rule≥0.900.946 live · 0.979 nightly
known_pattern confidence bandstable0.68–0.74, p10–p90
response questiongateable confp50 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)

SymptomCauseFix
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 rightForgot the restart after patchpatch → py_compile → restart → verify (always in that order)
laya-serve won't start after adding a ckptHF_HUB_OFFLINE=1 + not preloadedPreload manually first (§2.2), then restart
Inference suddenly slowThreads oversubscribed / concurrent pollers on single workerLAYA_THREADS ≤ physical cores; queue is by design
laya-serve --help "hangs" with 5 GB RAMIt's not --help — it booted the serverCtrl-C, configure via env vars in the unit
Confidence looks uncalibrated for some choicechoice:11+ bin ships out-of-range temp (0.5)Expected until Phase 4 calibration re-fit

7 · Files this demo touches (complete map)

FileWhat
~/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.