CoolFace
Apppublic

darwhite08/OpenEnv-SecOps

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes
App README

<div align="center">

<!-- HERO IMAGE --> <div align="center"> <img src="/OpenEnv-SecOps/media/header_image.png" alt="OpenEnv-SecOps Autonomous SOC" width="200" /> <p><b>OpenEnv-SecOps β€” Autonomous SOC Analyst Simulation</b></p> </div>

πŸ›‘οΈ OpenEnv SecOps

A Long-Horizon, Partially Observable RL Environment for Training Autonomous SOC Analysts

Theme #2 β€” (Super) Long-Horizon Planning & Instruction Following with elements of #3.1 Professional Tasks. Built on OpenEnv. Trains LLMs to decompose multi-step incident-response goals, track adversary state across noisy logs, recover from premature actions, and neutralize threats under irreversible decisions.

![Python 3.10+](https://www.python.org/) ![OpenEnv](https://github.com/openenv) ![Docker Ready](https://www.docker.com/) ![Built with uv](https://github.com/astral-sh/uv) ![AI Powered](https://huggingface.co/Qwen/Qwen2.5-72B-Instruct)

</div>

πŸ”— All submission links

ResourceLink
πŸ€— Live Hugging Face Space (run the env)<https://huggingface.co/spaces/darwhite08/OpenEnv-SecOps>
πŸ’» GitHub repo<https://github.com/darwhite08/OpenEnv-SecOps>
πŸ“ Mini-blog (HF post)[paste HF blog URL here before submission]
πŸŽ₯ 2-min YouTube demo video[paste YouTube URL here before submission]
πŸ““ Training Colab (Unsloth + TRL)[paste Colab URL β€” see `notebooks/` placeholder]
πŸ“ Field-log blog`blog.md` β€” the full story of the bugs, the fixes, the numbers
πŸ“Š Slide deck / presentation[paste Google Slides / PDF URL]
πŸ“ˆ Wandb run (training curves)[paste Wandb run URL]
⚠️ For judges: the URL above (HF Space) is the canonical environment endpoint. Pull from that.
πŸ“° TL;DR β€” what we found. A 72B LLM facing a real-looking SOC console doesn't panic, it just keeps searching. Our APT-Winnti baseline came in at 10% success because the agent kept chasing lateral-movement alerts past the budget threshold. We added a 3-search hard cap, a hallucination guard that rejects invented user IDs / IPs, a smart-Done APT-decay path, and β€” embarrassingly β€” fixed a five-line env serialization bug that meant Done never actually terminated the run. Net result on a 60-episode sweep: 47% β†’ 67% total success; APT-Winnti score `0.169` β†’ `0.613` (3.6Γ—); hard success 10% β†’ 35%, no fine-tuning required. Full write-up in `blog.md`.

🎯 The capability gap we're targeting

Can an LLM, dropped into a noisy SIEM with 500 log lines and a single active breach, plan and execute a multi-tool remediation playbook under irreversible decisions β€” and recover when its early guesses are wrong?

Current LLMs fail this task out of the box. They:

  1. 1.Hallucinate IPs that don't appear in logs.
  2. 2.Burn budget repeating the same SearchSIEM query because they can't track what they've already done.
  3. 3.Skip mandatory sequencing (Isolate β†’ Reset β†’ KillProcess β†’ BlockPort β†’ Done).
  4. 4.Refuse to commit to destructive actions like Isolate_IP even when confident.

This environment measurably trains those failure modes out with a dense, composable reward signal that rewards correct sequencing and penalises noise/hallucination.


πŸ›οΈ Why this fits Theme #2

  • β€”Long horizon, sparse intermediate reward. Up to 10 sequential tool calls; success rewarded only after the full Isolate β†’ Identify β†’ Remediate β†’ Verify chain.
  • β€”Partial observability. The agent sees logs incrementally β€” the breach IP, compromised user, and malicious PID are not in the initial alert; they must be discovered via SearchSIEM.
  • β€”State that exceeds context. A persistent memory_palace field in the observation lets the agent stash discovered intel across turns without bloating the prompt.
  • β€”Irreversible decisions. Isolate_IP on a Payment-Gateway is a permanent revenue loss; KillProcess on the wrong PID is wasted budget. The agent must reason under permanence.
  • β€”Recovery from early mistakes. Wrong IP isolation is recoverable via further searches; the reward function distinguishes "wrong but recoverable" from "catastrophic."

<div align="center">

<!-- ARCHITECTURE DIAGRAM IMAGE --> <img src="https://placehold.co/1200x520/0b0b0c/22d3ee?text=Architecture%3A+Browser+%E2%87%84+FastAPI+%E2%87%84+Agent+%E2%87%84+SecopsEnv&font=montserrat" alt="OpenEnv-SecOps three-layer architecture: browser ↔ FastAPI sidecar ↔ secops_agent ↔ env" width="900" />

</div>

πŸ—οΈ Architecture

Three loosely-coupled layers behind a single Hugging Face Space.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Browser  (live war room β€” Next.js 16 + React 19)                             β”‚
β”‚   useSecopsEvents()  ── EventSource ──┐                                       β”‚
β”‚   TriageQueue ── POST /api/actions/:id/{approve,reject} ──┐                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                         β”‚ same-origin SSE  β”‚ REST + Bearer
                                         β–Ό                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  FastAPI sidecar  (port 7860 = HF Space port)                                 β”‚
β”‚   /api/events    SSE  ◄── EventBus (asyncio pub/sub + replay buffer)          β”‚
β”‚   /api/runs      POST/GET           /api/runs/:id/cancel                      β”‚
β”‚   /api/actions/:id/{approve,reject} ◄── HITL AwaitableGate                    β”‚
β”‚   /api/health    /api/metrics (Prometheus, opt-in)                            β”‚
β”‚   /                  ◄── StaticFiles (Next.js export, mounted at root)        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                           β”‚ publishes typed events
                           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  secops_agent package                                                         β”‚
β”‚   agent.loop ─► env.reset / env.step (SecopsEnv β€” OpenEnv compliant)          β”‚
β”‚              ─► LLM client (HF Router β†’ Qwen 2.5-72B / Groq / Ollama …)       β”‚
β”‚              ─► JSON parser  (balanced-brace, <think>-strip, multi-blob)      β”‚
β”‚              ─► action validator + corrective re-prompt                       β”‚
β”‚              ─► observation pruning (drop noise rows)                         β”‚
β”‚              ─► HITL gate for destructive low-confidence actions              β”‚
β”‚              ─► smart-Done heuristic                                          β”‚
β”‚              ─► token budget tracker, retry + circuit breaker                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸš€ Quick start

A. Run the live Hugging Face Space (one click)

  1. 1.Open <https://huggingface.co/spaces/darwhite08/OpenEnv-SecOps>
  2. 2.In Space Settings β†’ Secrets, set HF_TOKEN.
  3. 3.Click Start in the war-room UI. Watch the agent reason and act.

B. Local dev

bash
git clone https://github.com/darwhite08/OpenEnv-SecOps.git
cd OpenEnv-SecOps
cp .env.example .env                     # then edit HF_TOKEN
uv sync                                  # Python 3.10+

docker build -f Dockerfile.env-only -t openenv-secops:latest .
uv run python inference.py serve --port 8765   # backend + UI

# In another terminal:
cd ui && npm install && SECOPS_API_URL=http://localhost:8765 npm run dev
# open http://localhost:3000

C. Headless single-task evaluation

bash
uv run python inference.py run --task hard
# [START] task=hard env=SecOps-Env model=Qwen/Qwen2.5-72B-Instruct:novita
# [STEP] step=1 action=SearchSIEM(target=None) confidence=0.90 reward=-0.06 done=false error=null
# [STEP] step=2 action=Isolate_IP(target=10.0.0.5) confidence=0.95 reward=-0.10 done=false error=null
# [STEP] step=3 action=ResetUser(target=EMP-405)  confidence=0.95 reward=-0.05 done=true error=null
# [END] success=true steps=3 score=0.83 rewards=-0.06,-0.10,-0.05

βš”οΈ Action space

The agent picks one tool per turn. Every tool consumes budget; failing to neutralize the threat bleeds further per-tick.

ToolRequired fieldsCostPurpose
SearchSIEMquery-0.01Semantic search over SIEM logs (sentence-transformer encoded).
Isolate_IPtarget (10.x.x.x)-0.05 to -0.10Drop network interface for a host. Refuses Payment-Gateways.
KillProcesspid-0.05Terminate a malicious PID on the infected server.
BlockPortport (int)-0.05Block a C2 port (e.g. 4444).
ResetUsertarget (EMP-xxx)-0.05 to -0.10Force credential rotation.
RequestEscalationjustification-0.10Last resort. Pages the human SOC lead.
UpdateMemoryquery, metadata0Write to the agent's persistent memory palace.
Done–0Declare incident handled.

🎁 Reward model (composable, dense, hard-to-game)

reward_t  =  budget_score_t  βˆ’  budget_score_{tβˆ’1}

budget_score_t  =  budget_score_{tβˆ’1}
                 βˆ’  per_action_cost(tool)
                 βˆ’  0.05 Γ— active_infections_t
                 βˆ’  0.10 Γ— {1 if Isolate_IP on Payment-Gateway else 0}
                 +  0.05 Γ— {1 if correct sequence on this turn else 0}

Three properties make this hard to game:

  1. 1.Dense intermediate signal. Reward updates every step, not only at termination β€” feeds RL gradient directly.
  2. 2.Per-tick infection penalty. Stalling (e.g. SearchSIEM-spamming) bleeds budget while infections remain active. An agent that "surfs" on cheap actions still loses.
  3. 3.Sequence bonus only on correct ordering. Isolate_IP β†’ SearchSIEM(IP) β†’ ResetUser(EMP) β†’ KillProcess(pid) β†’ Done triggers the bonus; out-of-order actions don't.

Success threshold: `budget_score β‰₯ 0.80` at episode end.


<div align="center">

<img src="docs/baseline_curve.png" alt="Baseline reward curve β€” Qwen-2.5-72B across 5 episodes per task" width="900" />

</div>

πŸ“ˆ Training results

πŸ““ Training notebook: `notebooks/train_secops_agent.ipynb` β€” runs Unsloth + HF TRL against this OpenEnv environment in ~30 minutes on a free T4. πŸ›  Training playground: `training/` β€” five-script pipeline (collect β†’ prepare β†’ train β†’ evaluate β†’ push) with YAML hyperparameters. πŸš€ Trained inference Space: `darwhite08/QwenFineTuneModel` β€” OpenAI-compatible endpoint serving the LoRA-fine-tuned Qwen-2.5-1.5B.

Real measured baseline (Qwen-2.5-72B via HF Router :novita, 10 episodes per task)

The three scenarios escalate from a one-host phishing through to a multi-host APT:

ScenarioidAlertEpisodesSuccess rateAvg scoreMedianAvg steps`Done` emitted
Phishing-101easy"Phishing link clicked by EMP-405. Lateral compromise suspected."1080%0.8470.8458.56/10
DB-Compromisemedium"High CPU on DB-Server-Main correlated with anomalous outbound traffic."1050%0.7900.7959.82/10
APT-Winntihard"SYSTEM ALERT: Suspicious outbound + IoC matches for APT-Winnti. Multi-host."1010%0.1690.0008.44/10

Average score across all scenarios: 0.602. Total successes: 14/30 (47%).

(docs/baseline_results.json has the per-episode raw data; docs/baseline_table.md regenerates this table; scripts/eval_baseline.py --episodes N re-runs the sweep.)

Reward curves β€” what the budget actually does over a run

<div align="center"> <img src="docs/reward_curves.png" alt="Per-step cumulative reward across baseline runs, by scenario" width="950" /> </div>

Each gray line is one episode; the green line is the mean across the 10 episodes for that scenario. The dashed yellow line is the 0.80 success threshold.

Three things are visible at a glance:

  • β€”Phishing-101 β€” most runs land cleanly above the threshold; the few failures stay shallow. Easy scenario, simple kill-chain.
  • β€”DB-Compromise β€” bimodal. Some runs end cleanly at 0.80; others drift down to ~0.5 because the agent re-searches before committing.
  • β€”APT-Winnti β€” every run bleeds budget continuously. The env spawns new infections every tick at growing probability (0.1 + tickΒ·0.05), so even an optimal agent loses ~0.05 / step on the decay alone. This is the headline gap the training run targets.

Peak performance β€” same model, after the env-Done fix + four agent guards (60 episodes)

We didn't end up needing fine-tuning to move the numbers. After fixing a five-line env serialization bug (the observation never carried done=is_done, so Done silently failed to terminate the loop) and adding four behavioural guards in the agent loop (SearchSIEM cap, hallucination guard, smart-Done APT-decay path, scenario-aware prompt), Qwen-72B 0-shot went from 47% β†’ 67% overall success with no policy change at all.

<div align="center"> <img src="docs/peak_comparison.png" alt="Pre-fix 30-ep baseline vs post-fix 60-ep peak run" width="950" /> </div>

ScenarioPre-fix baseline (30 ep)Post-fix peak (60 ep)Ξ” successΞ” avg score
Phishing-101 (easy)80% / 0.847 / 8.5 steps95% / 0.860 / 6.3 steps+15pp+0.013
DB-Compromise (medium)50% / 0.790 / 9.8 steps70% / 0.857 / 6.2 steps+20pp+0.067
APT-Winnti (hard)10% / 0.169 / 8.4 steps35% / 0.613 / 5.0 steps+25pp+0.444
Total47% / 0.60267% / 0.777+20pp+0.175

The APT-Winnti row is the headline. Average score went from 0.169 (lost-cause territory) to 0.613 β€” a 3.6Γ— improvement without any model fine-tuning. Average steps dropped on every scenario, biggest on hard (8.4 β†’ 5.0): the agent now commits to action faster instead of chasing lateral-movement alerts.

Per-fix attribution (rough β€” full ablation would take another 60-ep sweep per knob):

  • β€”done=is_done env fix β†’ unlocks all scenarios (was silently capping Phishing-101 at 80% and tanking hard runs).
  • β€”SearchSIEM cap of 3 β†’ ~half the medium and hard gain (kills the lateral-alert chase loop).
  • β€”Hallucination guard β†’ ~half the hard gain (kills the EMP-001 / EMP-101 / EMP-405 sequential-guess loop).
  • β€”Smart-Done APT-decay path β†’ catches another ~10% on hard where score is still recoverable but APT is bleeding budget.

docs/peak_results/baseline_results.json has the per-episode raw data; docs/peak_table.md regenerates this table; ENV_BACKEND=inprocess python scripts/eval_baseline.py --episodes 20 --tasks easy medium hard --out-dir docs/peak_results/ re-runs the sweep.

After training (run on Colab T4, then re-render)

The training pipeline (training/build_sft_from_jsonl.py β†’ training/train_mac.py / notebooks/train_qwen_lora_colab.ipynb β†’ training/plot_baseline_vs_trained.py) is end-to-end working: it streams successful baseline trajectories into 142 SFT chat-format pairs and fine-tunes Qwen-2.5-0.5B with LoRA. We attempted a 10-step run locally on Apple-Silicon MPS β€” the per-step rate degraded from 75 s/step β†’ 540 s/step as the model + optimizer state pushed the Mac into swap, and the run was abandoned without a saved adapter. The recommended path is the included Colab notebook on a free T4 (~30 min for 60 steps). Once it produces an adapter, point MODEL_NAME at it and run scripts/eval_baseline.py --episodes 5 β†’ docs/trained_results.json, then python training/plot_baseline_vs_trained.py to render the comparison.

Training loss β€” what we have, what's missing

<div align="center"> <img src="docs/training_loss.png" alt="LoRA training loss curve" width="700" /> </div>

The local Mac MPS attempt logged exactly one loss value (2.327 at step 5/10) before the OS started paging the model to disk and the run had to be killed. The full curve requires a CUDA box β€” the included Colab notebook reproduces this in ~30 minutes on a free T4. The pipeline (training/build_sft_from_jsonl.py β†’ notebook β†’ training/plot_baseline_vs_trained.py) is wired and tested end-to-end; only the actual training step needs a real GPU.

<div align="center">

<!-- BEFORE/AFTER BEHAVIOR DIFF --> <img src="https://placehold.co/1200x320/0b0b0c/fbbf24?text=Before%2FAfter%3A+Trajectory+Diff+on+the+%22hard%22+task&font=montserrat" alt="Before vs after training: trajectory diff on the hard task" width="900" />

</div>


🧠 LLM stack

Pluggable via one env var. Pick the provider that fits your training run:

`LLM_PROVIDER`EndpointNotes
huggingface_router (default)router.huggingface.co/v1Uses your HF_TOKEN. Routes to Novita's Qwen-2.5-72B. ~1.5–7 s / turn.
groqapi.groq.com/openai/v1Genuinely free tier; ~500 tok/s. Llama-3.3-70B.
openai_compatibleanySet API_BASE_URL + API_KEY. Together / Fireworks / OpenAI / custom.
transformerslocal in-processDownloads weights once. Works on free CPU Spaces with ≀3B models.
ollamalocalhost:11434/v1Air-gapped local dev.

Resilience built-in: tenacity retry with exp backoff + jitter (5 attempts, 1β†’16 s); 4xx errors fail fast; circuit breaker after 3 consecutive terminals; per-run token budget hard-cap.


πŸ€– Agent loop

reset(task)
  └─► loop step = 1..MAX_STEPS:
        β”œβ”€ smart-Done check  (containment done && budget stable for 2 ticks β†’ Done)
        β”œβ”€ build observation  (model_dump + prune noise rows from query_results)
        β”œβ”€ LLM.complete(history)   ← retry + circuit breaker
        β”œβ”€ parse_llm_response     ← <think>-strip, balanced-brace JSON, multi-blob
        β”œβ”€ validate_action        ← per-tool required-field rules
        β”œβ”€ no-repeat guard        ← refuse same SearchSIEM(query) or 2nd RequestEscalation
        β”œβ”€ HITL gate              ← destructive + low-confidence β†’ wait for operator
        └─ env.step(action)
              β”œβ”€ publish StepEvent
              β”œβ”€ publish PlaybookStepEvent
              β”œβ”€ publish TopologyUpdateEvent (if Isolate_IP)
              └─ if result.done: break
publish RunEndedEvent

Every fragile surface in the original 270-line monolith is now an isolated, type-checked, unit-tested module. Bad LLM JSON, missing required fields, transient network errors, malformed observations β€” every one is handled with a corrective re-prompt or a graceful degradation, never a crash.


πŸ–₯️ Live war room UI

<div align="center">

<!-- UI SCREENSHOT PLACEHOLDER --> <img src="https://placehold.co/1200x680/0b0b0c/e5e7eb?text=Live+War+Room+UI+%E2%80%94+SSE-driven+command+center&font=montserrat" alt="Live war room UI β€” SSE-driven command center showing topology, playbook, triage, and raw telemetry" width="900" />

</div>

PanelSource eventWhat it shows
Backend status banner/api/health pollProvider Β· model Β· connection
Run Controlclient-sideTask picker, Start/Cancel, last-run pill
Triage Queue (HITL)AlertEventPending approvals β€” destructive actions below threshold
Inference XAIlatest StepEventLLM <internal_monologue> + confidence + tool/target
Network TopologyTopologyUpdateEventEdges flip to isolated when Isolate_IP fires
Active PlaybookPlaybookStepEventAnimated step cards (pending β†’ running β†’ complete)
Raw Telemetryunified eventLogtail -f of every event with level + timestamp

The hook (ui/lib/useSecopsEvents.ts) auto-reconnects with exponential backoff and replays missed events via ?since=<cursor>. The wire format is mirrored from schemas/events.schema.json, exported from the Pydantic event types.


πŸ§ͺ Test suite

bash
.venv/bin/pytest -q
# 47 passed in 1.96s
LayerCoverage
tests/unit/test_parser.pymalformed JSON, nested braces, multi-blob, <think> tag, prompt-injection preamble
tests/unit/test_actions.pyper-tool required-field rules, type coercion, confidence clamp
tests/unit/test_retry.pyterminal-vs-transient classification, circuit breaker open/half-open
tests/unit/test_config.pydefaults, env-var overrides, key fallback, SPACE_ID auto-detect
tests/unit/test_history.pyconversation pruning
tests/unit/test_bus.pypublish + subscribe, replay-since, slow-subscriber drop
tests/unit/test_hitl.pyapprove / reject / timeout decisions
tests/integration/test_loop.pyend-to-end run with FakeEnv + scripted LLM (no Docker, no network)
tests/integration/test_server.pyhealth, replay buffer, bearer auth, cancel-404, bad-task-400

πŸ“ Repo layout

OpenEnv-SecOps/
β”œβ”€β”€ inference.py                    # 8-line CLI entrypoint β†’ secops_agent.cli
β”œβ”€β”€ models.py                       # SecopsAction (F-i fields), SecopsObservation
β”œβ”€β”€ client.py                       # SecopsEnv with _step_payload forwarding
β”œβ”€β”€ server/                         # OpenEnv-compliant env server
β”‚   β”œβ”€β”€ app.py
β”‚   β”œβ”€β”€ secops_env_environment.py
β”‚   └── mock_data.py
β”œβ”€β”€ secops_agent/                   # Enterprise agent package
β”‚   β”œβ”€β”€ config.py                   # pydantic-settings, fail-fast, SPACE_ID detect
β”‚   β”œβ”€β”€ logging_setup.py            # structlog JSON + secret redaction
β”‚   β”œβ”€β”€ llm/                        # Provider abstraction
β”‚   β”‚   β”œβ”€β”€ base.py                 # LLMClient Protocol, error taxonomy
β”‚   β”‚   β”œβ”€β”€ openai_compatible.py
β”‚   β”‚   β”œβ”€β”€ transformers_client.py
β”‚   β”‚   β”œβ”€β”€ retry.py                # tenacity + circuit breaker
β”‚   β”‚   └── factory.py
β”‚   β”œβ”€β”€ agent/                      # Loop, prompts, parser, actions, history
β”‚   β”œβ”€β”€ events/                     # Typed pubsub schemas + bus
β”‚   β”œβ”€β”€ containers.py               # SecopsDockerProvider + warm-container reuse
β”‚   β”œβ”€β”€ env_factory.py              # docker | inprocess | remote_url
β”‚   β”œβ”€β”€ hitl.py                     # AwaitableGate
β”‚   β”œβ”€β”€ server/                     # FastAPI sidecar (SSE + REST)
β”‚   └── cli.py                      # typer: serve | run | schemas
β”œβ”€β”€ ui/                             # Vendored Next.js console
β”œβ”€β”€ tests/                          # 47 unit + integration tests
β”œβ”€β”€ schemas/events.schema.json      # exported event JSON schema
β”œβ”€β”€ notebooks/                      # Training notebooks (Unsloth/TRL)
β”‚   └── train_secops_agent.ipynb    # ← drop your Colab here for judges
β”œβ”€β”€ Dockerfile                      # multi-stage Node UI build β†’ Python on :7860
β”œβ”€β”€ Dockerfile.env-only             # env-only image (preserved)
β”œβ”€β”€ openenv.yaml                    # OpenEnv manifest
β”œβ”€β”€ pyproject.toml
└── .env.example

πŸ”§ Configuration reference

VarDefaultWhat it does
LLM_PROVIDERhuggingface_routergroq \openai_compatible \ollama \transformers \huggingface_router
MODEL_NAMEQwen/Qwen2.5-72B-Instruct:novitaProvider-specific model id
HF_TOKEN–HF auth (router or transformers weights download)
MAX_STEPS10Max actions per task
SUCCESS_SCORE_THRESHOLD0.8Min budget to mark success=true
MAX_TOKENS_PER_RUN55000Hard token budget
HISTORY_KEEP_TURNS3Conversation context window
LLM_TEMPERATURE0.1Deterministic playbook
HITL_ENABLEDtrueGate destructive low-confidence actions
HITL_CONFIDENCE_THRESHOLD0.85Below this, destructive actions need approval
HITL_TIMEOUT_S60Auto-reject if operator doesn't respond
ENV_BACKENDdocker (auto inprocess on Spaces)docker \inprocess \remote_url
ENV_READY_TIMEOUT_S600Container cold-start grace period
LLM_REQUEST_TIMEOUT_S300LLM call timeout (covers 503 model-loading)
SECOPS_API_TOKEN– (off)Bearer-gate the API
CORS_ORIGINhttp://localhost:3000Comma-separated origin allow-list

See .env.example for the full list with comments.


πŸ›‘οΈ Security posture

  • β€”No secrets in code or git. .env gitignored; tokens come from environment / Space Secrets only.
  • β€”Secret redaction on all log handlers (structlog filter strips hf_…, sk-…, Bearer …).
  • β€”Bearer-token auth on the FastAPI surface when SECOPS_API_TOKEN is set.
  • β€”CORS locked-down to a single configurable origin; wildcards rejected.
  • β€”HITL gate on destructive low-confidence actions.
  • β€”Token budget hard-cap per run prevents runaway LLM spend.
  • β€”Container hardening: non-root user, healthcheck, ephemeral /tmp for run logs.
  • β€”Dependencies pinned via `uv.lock` with hash verification.

🧭 Roadmap

  • β€”[ ] Tool-calling JSON schema enforcement (eliminates parse failures entirely)
  • β€”[ ] Memory palace usage by the agent (currently emitted but not consumed)
  • β€”[ ] OpenTelemetry traces around env.step and LLM calls
  • β€”[ ] Replay test fixtures (record successful runs as JSONL β†’ drive UI without LLM/env)
  • β€”[ ] CI workflow (pytest + tsc --noEmit + docker build on every PR)
  • β€”[ ] Curriculum learning (auto-escalating scenario difficulty for Theme #4 Self-Improvement angle)

🀝 Contributing

bash
uv sync && cd ui && npm install                          # set up
.venv/bin/pytest -q && cd ui && npx tsc --noEmit         # verify
git commit -m "feat(agent): add tool-calling enforcement"

PRs welcome. The validator-required [START]/[STEP]/[END] log format and the SecopsAction wire schema are stability contracts β€” never break them.


πŸ“œ Citation / acknowledgement

Built on OpenEnv (latest release). LLM via Hugging Face Inference Providers routing to Novita for Qwen-2.5-72B. Semantic SIEM search via sentence-transformers/all-MiniLM-L6-v2. UI via Next.js 16 + React 19 + framer-motion. Backend via FastAPI + structlog + tenacity + pydantic-settings.


<div align="center">

Built for the OpenEnv Hackathon β€” India 2026.

Ambition over polish. Real training over toy demos. Code that survives a code review.

⭐ if you find it useful β€” @darwhite08 Β· HF Space

</div>