-
Notifications
You must be signed in to change notification settings - Fork 0
Pre‐Release Manual Testing
A pre-release checklist for validating an eunox-mcp release candidate on macOS,
Windows, and Linux. Enforcement behavior is identical across platforms — what
differs is the delivery surface (install channel, archive format, signature/checksum
verification, first-launch OS guard, and file paths), so the shared steps are written
once and per-OS variants are called out inline.
.goreleaser.yml builds every OS for both amd64 and arm64 — test each arch on
matching hardware if you ship both.
| macOS | Windows | Linux | |
|---|---|---|---|
| Install channel | Homebrew cask (stable tags only) | winget (stable tags only) |
.deb / .rpm / tarball |
| Archive | .tar.gz |
.zip (eunox-mcp.exe) |
.tar.gz |
| Checksum tool | shasum -a 256 |
Get-FileHash |
sha256sum -c (native) |
| First-launch guard | Gatekeeper quarantine | Mark-of-the-Web / SmartScreen | none |
Audit dir (~/.eunox) |
~/.eunox |
%USERPROFILE%\.eunox |
~/.eunox |
| Key-mode check |
stat -f '%Sp' → -rw-------
|
icacls (ACL, not POSIX) |
stat -c '%a' → 600
|
The released binaries for macOS and Windows are unsigned (per
.goreleaser.yml), so users hit Gatekeeper / SmartScreen on first launch; Linux has no equivalent. The first-launch step below documents the unblock for each.
# macOS (Homebrew):
brew install go jq cosign gh# Windows (winget); commands below are PowerShell:
winget install --id sigstore.cosign; winget install --id GitHub.cli; winget install --id jqlang.jq# Linux (apt/dnf); install cosign + gh per their upstream instructions:
sudo apt-get install -y jq # or: sudo dnf install -y jqgo (>= 1.25) is only needed for the source build in Step 1; node is for the npx
filesystem-server smoke test in Step 5.
# macOS / Linux:
cd eunox
make check-fmt && make check-license && make check-notice
make test # go test -race -count=1 ./...
make build # -> bin/eunox-mcp
./bin/eunox-mcp version # a source build prints "dev" -- expected here# Windows (no make; use the raw go commands):
cd eunox
go test -race -count=1 ./...
go build -o bin\eunox-mcp.exe .\cmd\eunox-mcp
.\bin\eunox-mcp.exe version # prints "dev" -- expected for a local buildThe
devstring is correct only for a local build. On the released binary the version must be the real tag.
Download the candidate's artifacts + signature chain (replace <tag>, e.g. v0.1.0-rc1).
Swap the OS pattern as needed (darwin / windows / linux):
mkdir -p ~/eunox-rel && cd ~/eunox-rel # PowerShell: mkdir ~\eunox-rel; cd ~\eunox-rel
gh release download <tag> --repo eunolabs/eunox \
--pattern 'eunox-mcp_*_<os>_*' \
--pattern 'checksums.txt' --pattern 'checksums.txt.pem' --pattern 'checksums.txt.sig' \
--pattern '*<os>*.sbom.json'
# Linux native packages, additionally:
# --pattern 'eunox-mcp*.deb' --pattern 'eunox-mcp*.rpm'2a. Verify the Sigstore signature on checksums.txt (same on every OS; PowerShell
uses backtick line-continuations instead of \):
cosign verify-blob \
--certificate-identity-regexp "^https://github\.com/eunolabs/eunox/\.github/workflows/release\.yml@refs/tags/" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
checksums.txt
# expect: "Verified OK"2b. Verify the downloaded artifact against the proven checksum file — the one
command that differs per OS (hashes in checksums.txt are lowercase):
# Linux (the form documented in SECURITY.md):
sha256sum -c checksums.txt --ignore-missing # every downloaded file prints ": OK"# macOS (no sha256sum; compare manually):
shasum -a 256 eunox-mcp_*_darwin_arm64.tar.gz
grep darwin_arm64 checksums.txt # the two hashes must match# Windows (no sha256sum):
(Get-FileHash .\eunox-mcp_*_windows_amd64.zip -Algorithm SHA256).Hash.ToLower()
Select-String -Path checksums.txt -Pattern 'windows_amd64' # the two hashes must match2c. Unpack, clear the first-launch guard, and confirm the real version — the other per-OS step:
# macOS -- Gatekeeper quarantine. Reproduce a real download, then clear it:
tar xzf eunox-mcp_*_darwin_arm64.tar.gz
xattr -w com.apple.quarantine "0081;00000000;Safari;" ./eunox-mcp
./eunox-mcp version # EXPECT: blocked ("cannot be opened")
xattr -d com.apple.quarantine ./eunox-mcp
./eunox-mcp version # EXPECT: the real <tag>, not "dev"# Windows -- Mark-of-the-Web / Defender SmartScreen:
Expand-Archive .\eunox-mcp_*_windows_amd64.zip -DestinationPath .\win
Get-Item .\win\eunox-mcp.exe -Stream Zone.Identifier # shows MotW on downloaded zips
.\win\eunox-mcp.exe version # EXPECT: SmartScreen may warn / block
Unblock-File .\win\eunox-mcp.exe
.\win\eunox-mcp.exe version # EXPECT: the real <tag>, not "dev"# Linux -- no quarantine; just confirm the executable bit survived and it runs:
tar xzf eunox-mcp_*_linux_amd64.tar.gz
./eunox-mcp version # EXPECT: the real <tag>, not "dev"Record the exact wording of any Gatekeeper / SmartScreen dialog — it's what users hit
first, and what release notes (and the Homebrew caveats) should pre-empt.
2d. SBOM present & parseable (grype sbom:<file> / trivy sbom <file> optional):
jq -e '.spdxVersion' eunox-mcp_*_<os>_*.sbom.json >/dev/null && echo "SBOM ok"
# PowerShell: Get-Content .\eunox-mcp_*_windows_*.sbom.json | ConvertFrom-Json | Select spdxVersionCasks and winget publish only on stable tags (skip_upload: auto skips rc/pre-release).
# macOS:
brew install eunolabs/tap/eunox-mcp
which eunox-mcp # /opt/homebrew/bin (arm64) or /usr/local/bin (Intel)
eunox-mcp version # real tag; note whether the cask clears the quarantine# Windows (open a fresh shell afterward so PATH refreshes):
winget install eunolabs.eunox-mcp
eunox-mcp version # real tag; note whether install clears SmartScreen# Linux -- confirm binary at /usr/bin and docs at /usr/share/doc/eunox-mcp/:
sudo dpkg -i eunox-mcp_*_linux_amd64.deb # or: sudo rpm -i eunox-mcp-*_linux_x86_64.rpm
which eunox-mcp; ls /usr/share/doc/eunox-mcp/ # LICENSE + README.mdSubcommands: proxy validate init suggest kill audit-verify stats doctor version.
eunox-mcp --help
eunox-mcp doctor # redacted support bundle; nothing uploaded
eunox-mcp validate demo/manifest.yaml # from a checkout; expect "OK" (Windows: demo\manifest.yaml)doctor prints os/arch: <goos>/<goarch> — confirm it matches the build you installed,
and that it redacts secrets.
Point an MCP host at the proxy. Host-config locations differ:
-
macOS:
~/Library/Application Support/Claude/claude_desktop_config.json -
Windows:
%APPDATA%\Claude\claude_desktop_config.json(escape backslashes in JSON paths) -
Linux: Claude Desktop isn't available; use a Linux MCP host (VS Code + Copilot agent
mode, Cursor, Cline, Roo) — same
command/argsshape.
{
"mcpServers": {
"fs-wiretap": {
"command": "eunox-mcp",
"args": ["proxy", "--audit", "--",
"npx", "-y", "@modelcontextprotocol/server-filesystem", "<path>"]
}
}
}macOS: point
<path>at a dedicated scratch dir (e.g.mkdir ~/eunox-test), not your home folder or~/Desktop/~/Documents/~/Downloads/~/Pictures. Those are TCC-protected, so a recursive tool call (e.g. "find docx files in my home folder") makes the filesystem server touch them and macOS fires a cascade of privacy prompts attributed toeunox-mcp— it's the launched binary, and the server runs as its child. That's the OS consent layer, unrelated to eunox policy (eunox checks the MCP call's arguments, not the upstream's raw filesystem syscalls); a scratch dir avoids it.
Drive some tool calls, then inspect the tape and the audit dir:
eunox-mcp stats # per-tool allow/deny histogram
# macOS / Linux:
ls -l ~/.eunox/
stat -f '%Sp' ~/.eunox/audit.key # macOS -> -rw-------
stat -c '%a' ~/.eunox/audit.key # Linux -> 600# Windows: ~ resolves to %USERPROFILE%
Get-ChildItem $env:USERPROFILE\.eunox
icacls $env:USERPROFILE\.eunox\audit.key # 0600 is POSIX-only; confirm via ACL insteadConfirm: wiretap forwards everything, tools/call records carry full args, and
.../list calls are recorded as enumeration events (allow records named after the
method, no argument map). (No host handy? Drive it over HTTP via Step 7 or the demo
scripts in Step 10 — no host required.)
Graduate the Step 5 wiretap into enforcement against the same upstream.
6a. Draft a manifest from the tape, then review and tighten it — the draft describes
what the agent did, not vetted policy. Write it to a real reviewed path (not /tmp,
which is ephemeral and doesn't exist on Windows):
eunox-mcp suggest --output ./eunox-manifest.yaml # reads the wiretap audit tape
eunox-mcp validate ./eunox-manifest.yaml # static check; expect "OK"
# Windows (PowerShell): same commands with .\eunox-manifest.yaml6b. Write an enforcing config pointing at the same upstream you wiretapped:
# eunox.yaml
schemaVersion: "0.1"
transport: stdio
upstreams:
- name: filesystem
transport: stdio
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "<same path as Step 5>"]
policy: ["./eunox-manifest.yaml"]6c. Re-point the MCP host from wiretap to enforcement. In the same host config edited in Step 5, swap the wiretap args for the enforcing config (use an absolute path), then restart the host:
6d. Exercise the policy from the host and confirm: a permitted call succeeds; a tool
absent from the manifest is denied with AUTHORIZATION_FAILED; a call whose argument
violates a condition (wrong path, disallowed SQL op) is denied with CONDITION_FAILED.
Every denial returns a structured JSON-RPC error and the upstream is never called.
The canonical four outcomes —
read_file /reports/*allowed,write_filedenied, off-/reports/*read denied, non-SELECTquery_dbdenied — are wired end-to-end against the mock server in the Docker demo (Step 10). Run that for a fixed, scriptable allow/deny matrix without standing up a host.
eunox-mcp proxy --config demo/gateway.yaml & # transport: http (Windows: run in its own terminal)
curl -s localhost:3000/healthz | jq # status, sessions, auditDropped...
curl -s localhost:3000/metrics | grep eunox_ # Prometheus counters
eunox-mcp kill all --port 3000 # loopback /control/killWindows: PowerShell aliases
curltoInvoke-WebRequest. Usecurl.exe, orInvoke-RestMethod http://localhost:3000/healthzand(Invoke-WebRequest http://localhost:3000/metrics).Content -split "\n" | Select-String eunox_`.
Confirm /healthz, /metrics, /control/kill are loopback-only. Confirm a route
with neither policy: nor enforcement: audit makes the gateway fail closed at
startup (route named in the error).
eunox-mcp audit-verify # expect: "Checked N record(s): N valid, 0 invalid, 0 skipped."Tamper test: edit one byte of a value in the audit log (~/.eunox/audit.jsonl, or
%USERPROFILE%\.eunox\audit.jsonl on Windows), re-run audit-verify, confirm it flags
that record (and the rest of the chain) invalid; then restore/delete. Also confirm
--require-audit makes the proxy exit non-zero at startup when --audit-log points at
an unwritable path (no silent blind run).
With a manifest + live upstream, confirm startup drift warnings on stderr
(fm1 glob over-permission, fm2 dead reference, ...). Confirm --strict-drift turns
over-permission / dead-ref / serverVersion findings into a fatal startup abort,
and that a descriptionHash mismatch aborts with or without the flag.
eunox-mcp validate <m> --live --upstream-url <url> reports the same out of band.
The Docker demo is the fastest independent end-to-end cross-check (Unix shell + Linux containers — on Windows run it from WSL2):
make -C demo up
make -C demo allow # ALLOWED read_file /reports/q3.pdf
make -C demo deny # DENIED write_file (AUTHORIZATION_FAILED)
make -C demo deny-path # DENIED /etc/shadow (CONDITION_FAILED)
make -C demo deny-op # DENIED query_db DELETE (CONDITION_FAILED)
make -C demo audit
make -C demo gateway-up && make -C demo ci-test-gateway
make -C demo down && make -C demo gateway-downOn Linux, if the demo audit log throws a permission error, run
sudo chmod 777 demo/audit && sudo chown -R 0:0 demo/audit, thenmake -C demo up(noted indemo/README.md).
Verify the released image signature (bound to the manifest digest):
cosign verify \
--certificate-identity-regexp "^https://github\.com/eunolabs/eunox/\.github/workflows/release\.yml@refs/tags/" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/eunolabs/eunox-mcp:<tag># macOS / Linux:
rm -rf ~/.eunox ~/eunox-rel
brew uninstall eunox-mcp # macOS, if installed via cask
sudo dpkg -r eunox-mcp # Linux deb (or: sudo rpm -e eunox-mcp)# Windows:
Remove-Item -Recurse -Force $env:USERPROFILE\.eunox, ~\eunox-rel
winget uninstall eunolabs.eunox-mcpRemove the test entry from your MCP host config.
-
First-launch guard on the unsigned binary — macOS Gatekeeper (
xattr -d com.apple.quarantine) and Windows Mark-of-the-Web / SmartScreen (Unblock-File). Linux has none. -
Checksum command differs —
sha256sum -c(Linux) vsshasum -a 256(macOS) vsGet-FileHash(Windows). SECURITY.md only documents the Linux form. -
Both
amd64andarm64per OS — verify and run each on matching hardware. -
Version string is the real tag, never
dev, on the released binary. -
Audit-key permissions —
0600is POSIX (staton macOS/Linux); on Windows it doesn't map to ACLs, so checkicacls/ Properties instead. -
~/ audit dir —~/.eunoxon macOS/Linux,%USERPROFILE%\.eunoxon Windows. -
Loopback-only endpoints —
/healthz,/metrics,/control/killmust refuse a non-loopback bind on every OS.