- Bind to 0.0.0.0 inside containers (DOCKER=true env marker)
- Healthcheck was failing because localhost resolved to IPv6 ::1
but server was bound to IPv4 127.0.0.1
- External exposure still protected via Coolify port mapping to 127.0.0.1
- Add Dockerfile with Node.js 18 alpine build
- Add docker-compose.yml with environment configuration
- Add docs/DOCKER.md deployment guide
- Update README.md with Docker quick start section
- Server binds to 0.0.0.0:3333 for external access
- UI available at http://localhost:3333/ui/
- Volumes mounted for reports and evidence persistence
Two issues blocked OpenAI-compatible servers that aren't Ollama-on-/v1:
1. LocalAdapter.isOpenAIWire matched only a literal /v1 path, so versioned
endpoints like Zhipu/z.ai's /api/paas/v4 silently fell through to Ollama's
native /api/chat wire format and failed. Match any /vN instead of literally /v1.
2. The `local` provider was treated as always-keyless, so servers that require
a bearer (Zhipu/z.ai, Together, ...) could never authenticate. Read an
optional key from TEMPEST_LOCAL_API_KEY (ZAI_API_KEY / ZHIPUAI_API_KEY as
aliases), wire it through getApiKey/hasApiKey so `tempest test` and the
mission engine accept a configured local provider, and document the options
in the env template.
The key is read from env in-memory and never persisted into the store, matching
the existing env-key hardening invariant (api-key-env-static).
Adds src/__tests__/local-openai-wire-static.test.ts pinning both fixes.
Verified end-to-end against z.ai's GLM models (glm-4.5-air / 4.6 / 5.2).
The arsenal advertises "83 tools" and verify-claims defends it by counting
`id:`/`name:` source lines — a source-line count that would keep passing even
if an entry became uncallable. Add a guard that ties the number to the real
registered arrays (TOOL_ADAPTERS + BUILTIN_TOOLS + EXTERNAL_TOOLS === 83) and
pins the invariant that makes the count honest:
an adapter is off the generic callable surface (adapterToCustomTool → null)
IFF it is explicitly gated by execution mode (catalog_only / import_only).
So a tool can be counted-but-not-generically-callable only on purpose —
metasploit / hydra (catalog_only, reachable solely via the approval-gated
post-ex drivers) and bloodhound (import_only) — never through a silent DEFAULT
or mint slip. Also asserts isMintable agrees with actual callability, the gated
set is exactly {metasploit, hydra, bloodhound}, and every minted adapter has a
real handler and a unique name. Sibling of no-phantom-tools / stub-honesty.
The opt-in code/supply-chain scanners (semgrep, gitleaks, trufflehog, trivy,
syft, grype, checkov) are subcommand-driven and non-networked, but had no
bespoke arg template — so adapterToCustomTool fell through to DEFAULT_TEMPLATE
and spawned `<binary> <target>`. For these that is a broken invocation:
`semgrep <host>` never runs a scan, and with no target it is `semgrep ''`.
semgrep in particular is advertised on the arsenal's headline tool list.
Add per-binary templates that mirror each tool's catalog commandHint and
operate on a filesystem PATH (default: the working dir "."):
semgrep → scan --config auto --json <path>
gitleaks → detect --source <path> --report-format json --redact --no-banner
trufflehog → filesystem <path> --json --no-update
trivy → fs --format json <path>
syft → dir:<path> -o cyclonedx-json
grype → dir:<path> -o json
checkov → -d <path> -o json
Subcommand + output flags are hardcoded (never taken from an LLM string),
matching the conservative pattern of the existing templates. The path is the
only tunable and is sanitised by scanPath(): a leading '-' (flag injection) or
an inherited http(s) mission target is rejected in favour of ".". Option-looking
targets are still refused up front by the factory's existing guard (no spawn).
Adds table-driven tests asserting the exact argv for each scanner, the "."
default, http(s)-target rejection, and the option-looking-path refusal.
verify-claims credits a solve straight from the committed `verdict`
(detected / score===1). Nothing asserted that a scored solve actually
agrees with its own committed flag oracle, so a hand-edited `detected:true`
(or a merge slip / fabricated placeholder flag) would pass verification
silently. The case-insensitive oracle comparator `ciMatch` was defined but
never called — this is exactly the check it was meant for.
Add an INTEGRITY GATE that re-derives, for every scored solve across the
pinned golden + white-box XBEN sweeps and the Cybench per-run artifacts,
that it is oracle-grounded: EITHER the self-reported flag matches the
committed `expected` (ciMatch), OR the harness auto-detected `expected`
verbatim in tool output (auto_detected, reported may be "UNKNOWN").
Anything scored that is neither is a phantom pass and fails the build.
Also rejects a fabricated placeholder credited via the self-report path,
and reports how much of the Cybench headline is backed by an oracle-bearing
per-run artifact vs. counted only in the hand-editable aggregate.
Adds src/__tests__/oracle-consistency.test.ts in the same spirit as
no-phantom-tools / stub-honesty: audits the committed artifacts AND proves
the gate has teeth against synthetic tampered/fabricated verdicts.
Current receipts pass clean: 0 phantom / 606 scored solves, 0 fabricated.
Three new category loadouts, each = arsenal + an offline-scorable benchmark, opt-in and
honestly scoped (roster marks all 🚧; pacu/frida stay catalog_only, unit-tested non-mintable):
- cloud: aws/az/gcloud + scoutsuite/cloudfox/pmapper; cloud:bench IaC-misconfig detection
- mobile: apkleaks/mobsfscan/drozer/objection/class-dump; mobile:bench static ruleset
- binary/RE: ghidra/objdump/checksec/strings/readelf/gdb; binary:bench decompiled-sink detector
Mobile/binary benches are DETECTOR REGRESSION CHECKS (rules+fixtures co-authored — a pass is a
floor, not a detection rate). No new headline numbers; verify-claims 24/24.
tsc clean; 337 tests; lint 0 errors.
Model IDs confirmed against xAI's published list (docs.x.ai/docs/models, 2026-07-05):
grok-build-0.1 (coding, 256K) and grok-4.3 (general, 1M). Addresses PR review P2.
The honesty pass had the hero paragraph and the benchmarks 'flex' block
restating numbers/caveats already in the Benchmarks table. State each once:
the hero drops the inline CVE-Zero deep-dive (kept in the table) and points to
#benchmarks; the flex block trims to its thesis. No claim/number changes.