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:

  1. ATHENA_TARGET is set (which target to attack)
  2. ATHENA_TOOL_REGISTRY file exists (what tools are available)
  3. ATHENA_ALLOWLIST file exists (approved targets only)
  4. allowlist.sha256 exists (integrity verification)
  5. 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:

  1. Parses --target and --config-dir from CLI
  2. Loads llm.toml for the LLM backend configuration
  3. Loads the target config (host, port, protocol, safe ranges, max actions)
  4. Verifies the allowlist SHA-256 hash matches
  5. Confirms the target is in the allowlist
  6. Health-checks the LLM backend (3 retries, exponential backoff: 1s, 2s, 4s)
  7. Builds all orchestrator components (tool registry, rate limiter, ground-truth emitter, traffic labeler)
  8. Runs AgentOrchestrator.run_scenario()
  9. 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:8b loaded
  • A target (Juice Shop) reachable on the container’s network
  • The Rust tool binaries (need Cargo.lock committed 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 fix
  • athena-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.