Work / Nexus / Changelog / Day 1
Day 1: Wiring the Agent
Making ./scripts/run-athena-profile.sh agent juice-shop actually work. Multi-stage Docker builds, an entrypoint that validates before it executes, and a Python CLI that bootstraps the OPAR loop.
Day 0 was architecture, specs, and UI. Day 1 is plumbing — making the LLM agent actually run inside a container with one command.
The goal: ./scripts/run-athena-profile.sh agent juice-shop starts a Docker container that autonomously probes a target using an Observe/Plan/Act/Reflect loop, with safety controls enforced at every step.
The problem
The agent code (athena-agents) lives in one repo. The container image (nexus-athena) lives in another. The agent needs Python for the orchestrator, Rust binaries for protocol tools (Modbus, CAN Bus), and a config directory with targets, allowlists, and LLM settings. Docker can’t follow symlinks in build contexts, and compose volume paths resolve relative to the compose file — not your working directory.
Three repos need to cooperate: the image, the agent code, and the config.
Multi-stage Dockerfile
The unified Dockerfile now has four stages:
rust-builder → compile Rust crates (athena-modbus, athena-canbus)
python-builder → pip install the orchestrator package
athena-core → Kali base + tools + COPY from both builders
athena-full → extends core with Metasploit, Wireshark, radare2 The rust-builder compiles from athena-agents/crates/. The python-builder installs the orchestrator package from athena-agents/pyproject.toml. Both stages output their artifacts into /out or /install paths, which the athena-core stage COPYs in.
Because Docker can’t follow symlinks, I wrote scripts/build-athena-image.sh that creates a temporary build context by copying the necessary files from both repos into a temp directory, then runs docker buildx build against it. Clean separation, no workspace coupling.
The entrypoint script
Before the Python orchestrator runs, a shell script validates everything:
athena-agent-entrypoint.sh [--dry-run] It checks in sequence:
ATHENA_TARGETis set (which target to attack)ATHENA_TOOL_REGISTRYfile exists (what tools are available)ATHENA_ALLOWLISTfile exists (approved targets only)allowlist.sha256exists (integrity verification)- Target config file exists at
targets/{name}.toml
If any check fails, you get a structured error:
ERROR: [athena-agent-entrypoint] ATHENA_TARGET not set
Expected: A target identifier matching a file in /opt/athena/config/targets/
Got: (empty) No ambiguity. No silent failures. If everything passes, it sets PYTHONUNBUFFERED=1 and execs into the Python orchestrator.
The --dry-run flag validates without launching — useful for CI and smoke tests.
orchestrator/main.py
This is the bridge between the entrypoint script and the OPAR loop. It:
- Parses
--targetand--config-dirfrom CLI - Loads
llm.tomlfor the LLM backend configuration - Loads the target config (host, port, protocol, safe ranges, max actions)
- Verifies the allowlist SHA-256 hash matches
- Confirms the target is in the allowlist
- Health-checks the LLM backend (3 retries, exponential backoff: 1s, 2s, 4s)
- Builds all orchestrator components (tool registry, rate limiter, ground-truth emitter, traffic labeler)
- Runs
AgentOrchestrator.run_scenario() - Writes a final summary record to the ground-truth JSONL
Exit codes are meaningful: 0 for success, 1 for failure, 130 for keyboard interrupt.
Compose wiring
The compose profile mounts the config directory read-only and provides a named volume for ground-truth output:
athena.agent:
command: ["/usr/local/bin/athena-agent-entrypoint.sh"]
volumes:
- ../../config:/opt/athena/config:ro
- athena_output:/opt/athena/output
environment:
- ATHENA_TARGET=${ATHENA_TARGET:-}
- OLLAMA_HOST=${OLLAMA_HOST:-http://host.docker.internal:11434} The ../../config path is relative to the compose file at deploy/compose/, not the repo root. This took one failed build to figure out — compose volume paths resolve from the file’s location, always.
The run script
./scripts/run-athena-profile.sh agent juice-shop The second argument becomes ATHENA_TARGET. If you forget it, the script lists available targets from config/targets/:
Usage: ./scripts/run-athena-profile.sh agent <target>
Available targets:
juice-shop
openplc What’s verified
All of this was tested inside the container:
$ docker run --rm \
-v ./config:/opt/athena/config:ro \
-e ATHENA_TARGET=juice-shop \
-e ATHENA_TOOL_REGISTRY=/opt/athena/config/tool-registry.toml \
-e ATHENA_ALLOWLIST=/opt/athena/config/allowlist.json \
phoenixvlabs/nexus-athena:latest \
/usr/local/bin/athena-agent-entrypoint.sh --dry-run
=== athena-agent-entrypoint: dry-run validation passed ===
ATHENA_TARGET: juice-shop
ATHENA_CONFIG_DIR: /opt/athena/config
OLLAMA_HOST: http://host.docker.internal:11434
TARGET_CONFIG: /opt/athena/config/targets/juice-shop.toml The Python orchestrator module imports correctly inside the container. The entrypoint validates. The compose mounts work.
What’s not done yet
The OPAR loop hasn’t run live. That requires:
- Ollama running with
llama3:8bloaded - A target (Juice Shop) reachable on the container’s network
- The Rust tool binaries (need
Cargo.lockcommitted for reproducible builds)
That’s Day 2. The wiring is complete. Now it needs something to talk to.
Token savings: zero so far
No scenarios have run, so no token measurement yet. But the infrastructure for measuring is in place: every scenario will emit ground-truth JSONL with action counts and timing. When skills start loading into the Plan phase context, the before/after comparison becomes automatic.
Commits
nexus-athena: Dockerfile (builder stages), entrypoint script, build script, compose wiring, config volume fixathena-agents:orchestrator/__main__.py(CLI, config loading, LLM health check, OPAR execution)
Day 1: 6 commits, ~500 lines, zero runtime yet. All validation, all plumbing, all safety checks. The agent runs tomorrow.