Repository navigation
chore: stage ARTEX upstream hardening and operational readiness gate - #1
Draft
thebrierfox wants to merge 53 commits into
Draft
thebrierfox wants to merge 53 commits into
thebrierfox wants to merge 53 commits into
Conversation
Two review fixes on top of the Ollama provider: FromEnv no longer inherits OPENAI_API_KEY for the ollama format. Ollama ignores the bearer token, so inheriting the cloud key could only ship it to the local or LAN endpoint, contradicting the comment in NewProvider and the documented behavior. ConfigFrom now normalizes the base URL to the /v1 root, so the natural Ollama URL (host:11434 without /v1) works for chat as well. Previously the provider appended /chat/completions to it and the request landed on a path Ollama does not serve, while pgListModels already appended /v1. Tests: bare-host normalization assertion, the /v1/chat/completions path assertion, and a new test that the env config carries no inherited key.
ollama support
The Streamable HTTP client always offered 2025-06-18 and kept sending that MCP-Protocol-Version header after initialize, ignoring the version the server answered with. A server that only supports 2025-11-25 negotiates its own version and then rejects every later request with 400, so none of its tools load. Servers that validate the header on initialize itself reject the handshake outright, and the client never retried with another version. - Offer 2025-11-25 first and fall back to 2025-06-18 when the server rejects the version: HTTP 400 "Unsupported protocol version", JSON-RPC -32602, or an UNSUPPORTED_PROTOCOL_VERSION code - Send the protocolVersion from the initialize result on later requests - Keep the legacy HTTP+SSE transport on 2024-11-05 Closes BaaRai#131 Tags: #mcp Co-Authored-By: htjulia <htjulia1@gmail.com>
The toolbox entry claimed the tools were verified at build time. They are version-pinned and smoke-tested by running them once; nothing is checksum verified, sqlmap tracks the default branch, and the dalfox check cannot fail. Say that plainly and point at BaaRai#127, which adds checksum verification. Also fix two misattributions: BaaRai#65 is the deadline contract, not pause, and BaaRai#108 adds documentation rather than reporting code.
docker-compose.yml referenced autumn27/artex:latest. That repository is gone (404), but the Docker Hub account is alive, so whoever holds that name can publish the tag and every compose pull would run their image. A local review of the published artifacts found the same class of risk still present on the default install path after BaaRai#125 removed the in-app updater. Build from the checkout instead, and make update.sh stop when git pull cannot fast-forward: it used to warn, rebuild the old tree and print 'Build complete', which is what every clone made before the 2026-10-23 history rewrite now hits.
The saved-profile path normalized /v1 but FromEnv did not, so ARTEX_LLM_BASE_URL=http://host:11434 sent chat to /chat/completions and got a 404 on a LAN Ollama server. Share one helper between both entry points, and add an env-path test with a bare host and with a full endpoint URL. Also soften the keyless comment: a local Ollama ignores the bearer token, but a gateway in front of it can require one, which belongs in a saved profile.
The client sent MCP-Protocol-Version on the initialize request itself. The spec requires the header on requests after initialization, and the offered version already travels in the initialize body. A server that validates the header rejected the handshake unless the client happened to offer a version it supports, so connecting depended on matching the error body and only worked for the two versions the client offers. - Send the header only after initialize succeeds, with the version the server negotiated - Keep the fallback loop, newest-first order and negotiated-version persistence for servers that still reject the initialize body version - Rewrite both version tests around this, including a 2025-03-26-only server, and assert that initialize carries no version header Tags: #mcp Co-Authored-By: htjulia <htjulia1@gmail.com>
…ld processes spawn ARTEX_PG_DSN, ANTHROPIC_API_KEY and OPENAI_API_KEY are read only during startup (server.NewManager and agent.FromEnv in server.New), but stayed in the process environment. Agent shells, custom tools and MCP stdio servers copy os.Environ() when they start, so they could read the database password and the LLM keys. main now unsets the three variables right after server.New returns.
The scrub ran in main after server.New returned, but New resumes every non-terminal task before returning, and those loops start workers that run shells. Move the scrub into New, right after both environment readers, and start MCP discovery after it as well: both paths spawn child processes. New takes an option rather than scrubbing unconditionally. The test suite builds several servers in one process and still needs ARTEX_PG_DSN on later calls, so only the binary opts in.
fix(security): remove startup secrets from the environment before child processes spawn
…localhost POST /api/auth/init accepted any caller while auth.password_hash was empty, so whoever reached port 8787 first could set the admin password and take the instance. When no password is set at startup, the server now generates a random one-time setup token and writes it to the server log. authInit compares it in constant time, rejects a missing or wrong token with 403, and clears it once the password is stored. The setup page has a field for the token and api.initPassword sends it. docker-compose publishes 8787 on 127.0.0.1 only, like 8788. README says where to find the token.
Publishing 127.0.0.1:8787 removed the only working access path for installs reached through a host or WSL IP, without an obvious way back. Keep loopback as the default and let ARTEX_BIND_ADDR override it (0.0.0.0 restores the old behaviour; the setup token still protects the first-run claim).
fix(auth): require a setup token for /api/auth/init and bind 8787 to localhost
…nored Commit 2c8359f appended backups/ to the /Dockerfile.bench.v2 line, which had no trailing newline. The result, /Dockerfile.bench.v2backups/, matched neither file, so dumps written by scripts/backup.sh showed up as untracked and could be committed by accident.
With --keep 0, prune ran tail -n +1 over the newest-first listing and deleted every dump, including the one written seconds earlier, then reported success. The check also rejects leading zeros, which the $((KEEP + 1)) arithmetic would parse as octal (08 and 09 abort the script after the dump).
FromEnv compared the raw value, so "Ollama" or "OpenAI" matched no case and fell through to the anthropic default. With ARTEX_LLM_PROVIDER=Ollama and an ANTHROPIC_API_KEY set, the Anthropic key was sent to the Ollama base URL.
…lues config.Load discarded the json.Unmarshal error, so a config.json that did not parse was treated as absent: startup reported "no database config found" and skill_dir silently fell back to the default. Load now returns read and parse errors (a missing file is still not an error); PostgresDSN and SkillDir pass them up, and main exits with the message. install.sh wrote the database values into config.json unescaped, so a password containing " or \ produced invalid JSON, which the old Load then ignored. The values are now JSON-escaped and the port must be numeric. The .env sed replacements escape \, | and & so those characters no longer break the substitution or get rewritten by it.
POST /api/llm/test and POST /api/llm/models take provider and base_url from the form but fell back to the stored profile key (and, for the test, the global key) when the form had none. Any base_url typed into the form, or supplied by a crafted request, received the saved key. The saved key is now used only when the request's provider and base URL normalize to the ones it was saved with (sameLLMEndpoint, built on agent.ConfigFrom). Otherwise the key must be entered in the form, and the error says why.
The image installed Node 20 from NodeSource, which is end-of-life. CI and the release workflow already build the frontend with Node 22; the runtime image (Playwright MCP/CLI, npx-based MCP servers) now uses the same major.
go test -race runs on every package that passes under the race detector locally (all but ./server, which needs a -race check against postgres first). A new shell job runs shellcheck on install.sh, update.sh, start.sh and scripts/backup.sh.
Raises the go directive from 1.26.3 to 1.26.10, the newest 1.26 patch release (CI installs it from go.mod; release.yml's "1.26" already resolves to it; the Dockerfile ships a prebuilt binary and has no Go image). golang.org/x/net goes from v0.58.0 to v0.62.0, the latest; go mod tidy raises x/crypto, x/text, x/sys, x/sync, x/mod and x/tools to the versions it requires.
The first run reported only info-level notes (SC2015 in update.sh, SC2012 and SC2086 on the intentional glob in backup.sh), which made the job fail.
New accepted opts ...Option but never called them, so scrubSecrets stayed false in the binary and ScrubSecretEnv was unreachable: the protection merged in BaaRai#136 never engaged. The unit test passed because it called the scrub function directly instead of going through New. Apply the options in the constructor and add a test that builds a Server through New with WithSecretScrub and asserts the secrets are gone. Verified to fail without the loop.
fix(gitignore): ignore backups/ again
fix(backup): reject --keep 0
fix(agent): match ARTEX_LLM_PROVIDER case-insensitively
fix(docker): install Node 22 in the runtime image
build: go 1.26.10 and golang.org/x/net v0.62.0
fix(config): fail on malformed config.json; escape values in install.sh
fix(llm): send a saved API key only to the endpoint it was saved for
ci: add go test -race and shellcheck
fix(security): apply New options so the secret scrub actually runs
The -race step reused the job's ARTEX_PG_DSN, so it ran against the database the plain test step had just populated. The db tests are not idempotent across runs and failed on duplicate llm_profiles names, turning main red right after BaaRai#144 merged.
…Rai#147) The Dockerfile copied a pre-built binary from dist/<arch>/artex into the runtime image, but dist/ is git-ignored, so a fresh `git clone` has no such file and `docker compose up -d --build` fails with "/dist/amd64/artex: not found". Build the binary from source in the image instead: a Node 22 stage runs the frontend static export, a Go 1.26 stage runs build.sh with the same flags as the release binaries job (CGO-free cross-compile, -trimpath, pinned version), and the runtime stage copies the compiled binary. No build-context binary is needed, so a clean clone builds. Drop the now-obsolete dist/ warning in install.sh.
Move the Unreleased entries into a 0.5.0 section and add the changes merged since v0.4.0 (BaaRai#128, BaaRai#136-BaaRai#147), with known issues.
The server wrote jwt.key to the executable's directory, /app in the image, so it lived in the container's writable layer and every container rebuild generated a new key and signed all users out. - ARTEX_JWT_KEY_DIR selects the key directory (default: config.BaseDir(), unchanged for non-Docker installs). docker-compose.yml sets it to /app/keys and bind-mounts ./keys there; the Dockerfile declares the volume. The key stays out of data/, which the file manager browses. - loadOrCreateJWTKey creates the directory (0700). When keyDir/jwt.key is missing it migrates dataDir/jwt.key, then BaseDir()/jwt.key (mode 0600, old file removed), otherwise generates a new key. An existing key is never overwritten; a short one is now a startup error. - .gitignore and .dockerignore exclude keys/. - README and CHANGELOG updated; the 0.5.0 known issue is removed.
fix(auth): keep the JWT signing key in a dedicated volume
Added a Trendshift badge to the README.
…default
- POST /api/auth/login: 10 attempts per minute per TCP peer IP
(X-Forwarded-For ignored), then 429 with Retry-After. The block starts
at 1 minute and doubles on each attempt while blocked and on each new
block, capped at 15 minutes. A successful login clears the IP. No
per-account lockout. In-memory, idle entries swept after 15 minutes,
at most 10,000 IPs.
- New passwords (init, change-password, /setup page) need 12 characters.
Login does not check length, so existing 8-character passwords work.
- The binary's -addr default is 127.0.0.1:8787. The Docker image passes
-addr :8787 explicitly and is unchanged.
- GET /api/health returns {"ok":true} without a valid token; with one it
still returns service and version for the web UI top bar.
… reset-password.sh - ARTEX_TRUSTED_PROXIES (comma-separated CIDRs, empty by default). When the TCP peer is listed, the login limit reads X-Forwarded-For from the right and keys on the first hop that is not a trusted proxy; otherwise the header is ignored. Without it every client behind a reverse proxy shares one bucket and an attacker can block the admin. Passed through by docker-compose.yml and listed in .env.example. - README: nginx example sends X-Forwarded-For; document the variable; the binary's default listen address is 127.0.0.1:8787. - reset-password.sh refuses passwords under 12 characters, counted in characters like the server.
- README: ARTEX_TRUSTED_PROXIES for nginx and ARTEX on the host (127.0.0.1) and for nginx on the host with ARTEX in Docker (the bridge gateway, found with docker network inspect). - CI shell job: run shellcheck on reset-password.sh too.
fix(auth): rate-limit login per IP, trusted proxies, 12-character passwords
release: v0.5.1
…lback fix(mcp): negotiate protocol version with fallback
…E step The build stage of the Dockerfile runs `COPY . .`, so anything left in the checkout root is sent to the builder and copied into the build stage. Exclude the local secrets and backups that the install and backup scripts create there: .env, config.json, jwt.key and backups/. keys/ was already excluded. The Docker Compose (manual) section of the README still told users to build the single binary before `docker compose up -d --build`. Since the multi-stage Dockerfile builds the frontend and the Go binary itself, that step is no longer needed; remove it.
chore: keep secrets out of the Docker build context; drop stale README step
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scope
BaaRai/ARTEXata908e1ec170f973734b3258dd37d667d2562440a(includes v0.5.1) onto the fork's originalmainatb3717f7a5fe8d07fcd4fe7167603f314e9b64840.Why
The previous
docker-compose.ymlreferenced discontinuedautumn27/artexand bound 8787 to every interface; a fresh Dockerfile build also needed pre-existingdist/artifacts. Upstream transitions to locally built images, loopback defaults, and improved auth/secret handling. Recent ARTEX ecosystem trojanized-image reports make source/build provenance a priority.Must verify before merge or deploy
@latest, sqlmap branch, and downloaded tool checksums.Status
DRAFT — NOT CERTIFIED. Keep
mainand any deployment unchanged until evidence is recorded. Passing unit tests is necessary but does not establish operational readiness.Sources: BaaRai/ARTEX and elder-plinius/T3MP3ST.