Commit Graph
658 Commits
Author SHA1 Message Date
BaaRai 9481b8e962 fix(intercept): judge decisions were stored as unknown (prefix case mismatch)
Since e233c02 the judge writer prefixes its reason with "[model]" while
interceptSource, the approvalDecisionSource SQL fallback and the archive
restore checked "[Model]", so model-decided rows were inserted with
decision_source='unknown' and missed by the decision_source=model filter.

- db: add ModelReasonPrefix ("[model]") and isModelReason, which also
  accepts the pre-e233c02 "[模型]" prefix; readers compare
  case-insensitively (lower(reason) LIKE in SQL).
- intercept: the writer builds its reason through JudgeReason, using the
  shared constant.
- schema.sql: idempotent backfill of 'unknown' rows without a rule whose
  reason carries either prefix.
- tests: un-skip TestInterceptFilterHTTP (it failed only because of
  this), make the db filter fixture use the constant, and add a
  Postgres-free test pinning the writer output to interceptSource.

Fixes #156
2026-11-09 12:22:46 +01:00
BaaRai f90e8d2bcb Merge pull request #158 from BaaRai/docs/openai-responses-base-url
docs: openai-responses base URL suffix
2026-11-09 12:04:10 +01:00
BaaRai cab0a10c6f docs: openai-responses base URL suffix 2026-11-09 11:53:01 +01:00
BaaRai 180538e463 Merge pull request #157 from BaaRai/docs/anthropic-base-url
docs: Anthropic base URL must not end in /v1
2026-11-09 11:37:27 +01:00
BaaRai a7da0559f3 docs: Anthropic base URL must not end in /v1 2026-11-09 11:27:54 +01:00
BaaRai 9bf5d816ae Merge pull request #155 from BaaRai/feat/llm-user-agent
feat(llm): optional custom User-Agent for LLM requests
2026-11-08 18:26:58 +01:00
BaaRai 4acdf3bb26 fix(llm): validate custom User-Agent in the connection-test path
testLLM set cfg.UserAgent from the request (or the stored-profile
fallback) with no validation, while pgSaveProfile runs
agent.ValidateUserAgent and returns 400 on a bad value. A UA with a
control character could therefore reach the wire from the test button
though it is rejected on save. Call ValidateUserAgent on the final value
(after the stored fallback) and return 400 with the same message style,
mirroring pgSaveProfile; the fallback itself is unchanged.

ValidateUserAgent rejects every control character (r < 0x20 or r ==
0x7f), TAB included, so the header value stays a single printable token.
Extend the agent tests to cover NUL, US, DEL and TAB, and that a
printable slash-token value with spaces survives trimming unchanged. Add
a Postgres-free server test driving testLLM through an httptest endpoint:
a control-character UA is a 400 before any request leaves, a printable UA
reaches the wire verbatim.
2026-11-08 18:15:33 +01:00
BaaRai 13dde2da15 feat(llm): optional custom User-Agent for LLM requests
Add an optional per-profile User-Agent that overrides the header on every
outbound LLM request. When empty (the default), the existing behaviour is
unchanged and the default User-Agent is left untouched.

The value is trimmed and validated: control characters / newlines (header
injection) and values longer than 256 bytes are rejected. Wired through the
profile config struct, the Postgres llm_profiles schema, the profile API/DTO,
the connection-test path, and the LLM profile form, following the existing
session-header field. Addresses part of #154; the fixed-full-URL mode is not
included.
2026-11-08 17:11:43 +01:00
BaaRai a908e1ec17 Merge pull request #153 from BaaRai/chore/dockerignore-and-readme
chore: keep secrets out of the Docker build context; drop stale README step
2026-11-06 14:41:29 +01:00
BaaRai 9784bb192c chore: keep secrets out of the Docker build context; drop stale README 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.
2026-11-06 14:31:52 +01:00
BaaRai 44d755fb2e Merge pull request #132 from htcom-code/fix/131-mcp-version-fallback
fix(mcp): negotiate protocol version with fallback
2026-11-06 14:31:44 +01:00
BaaRai ce279d77d3 Merge pull request #151 from BaaRai/release/v0.5.1
release: v0.5.1
v0.5.1
2026-11-05 18:17:19 +01:00
BaaRai 41e10456d7 release: v0.5.1 2026-11-05 18:09:19 +01:00
BaaRai 43424404ee Merge pull request #150 from BaaRai/fix/login-rate-limit-hardening
fix(auth): rate-limit login per IP, trusted proxies, 12-character passwords
2026-11-05 18:08:11 +01:00
BaaRai cb13a22673 ci: retrigger after PR head sync 2026-11-05 17:30:24 +01:00
BaaRai 8beb9c50d6 fix(ci): drop unused variable in reset-password.sh (SC2034) 2026-11-05 17:11:41 +01:00
BaaRai 4be2fc5c9a docs, ci: proxy setup for host and Docker, shellcheck reset-password.sh
- 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.
2026-11-05 17:06:10 +01:00
BaaRai d0d153b768 fix(auth): trusted proxies for the login rate limit, 12 characters in 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.
2026-11-05 16:57:48 +01:00
BaaRai 2419038884 fix(auth): rate-limit login per IP, 12-character passwords, loopback 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.
2026-11-05 16:48:27 +01:00
BaaRai e6e9b2f978 Add Trendshift badge to README
Added a Trendshift badge to the README.
2026-11-04 21:29:45 +01:00
BaaRai 248d9d9673 Merge pull request #149 from BaaRai/fix/jwt-key-volume
fix(auth): keep the JWT signing key in a dedicated volume
2026-11-04 09:57:52 +01:00
BaaRai 65bdc5d73c fix(auth): keep the JWT signing key in a dedicated volume
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.
2026-11-04 09:18:11 +01:00
BaaRai fc35dddee8 release: v0.5.0 (#148)
Move the Unreleased entries into a 0.5.0 section and add the changes merged since v0.4.0 (#128, #136-#147), with known issues.
v0.5.0
2026-11-04 01:26:24 +01:00
BaaRai 668e099dd8 fix(docker): build the image from source so a fresh clone builds (#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.
2026-11-04 01:06:59 +01:00
BaaRai 04777513b6 ci: give the race run its own database
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 #144 merged.
2026-11-03 23:58:15 +01:00
BaaRai 90e7b3ef55 Merge pull request #146 from BaaRai/fix/apply-new-options
fix(security): apply New options so the secret scrub actually runs
2026-11-03 23:52:28 +01:00
BaaRai b100848ebf Merge pull request #144 from BaaRai/fix/ci-race-shellcheck
ci: add go test -race and shellcheck
2026-11-03 23:49:40 +01:00
BaaRai 0093781c8b Merge pull request #142 from BaaRai/fix/llm-stored-key-endpoint
fix(llm): send a saved API key only to the endpoint it was saved for
2026-11-03 23:49:33 +01:00
BaaRai e94471609f Merge pull request #141 from BaaRai/fix/config-load-errors
fix(config): fail on malformed config.json; escape values in install.sh
2026-11-03 23:49:26 +01:00
BaaRai 77e2c5f9fa Merge pull request #145 from BaaRai/fix/go-1.26.10-xnet
build: go 1.26.10 and golang.org/x/net v0.62.0
2026-11-03 23:49:20 +01:00
BaaRai 41fe40af07 Merge pull request #143 from BaaRai/fix/node-22
fix(docker): install Node 22 in the runtime image
2026-11-03 23:49:03 +01:00
BaaRai 0d9602dc56 Merge pull request #140 from BaaRai/fix/llm-provider-env-case
fix(agent): match ARTEX_LLM_PROVIDER case-insensitively
2026-11-03 23:48:56 +01:00
BaaRai 5e872ccc15 Merge pull request #139 from BaaRai/fix/backup-keep-zero
fix(backup): reject --keep 0
2026-11-03 23:48:49 +01:00
BaaRai d3c4d21086 Merge pull request #138 from BaaRai/fix/gitignore-backups
fix(gitignore): ignore backups/ again
2026-11-03 23:48:42 +01:00
BaaRai 69831f5cdf fix(security): apply New options so the secret scrub actually runs
New accepted opts ...Option but never called them, so scrubSecrets stayed
false in the binary and ScrubSecretEnv was unreachable: the protection merged
in #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.
2026-11-03 23:47:49 +01:00
GRAB Racem 3322ecd6a6 ci: run shellcheck at warning severity
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.
2026-11-03 23:36:00 +01:00
GRAB Racem 7b2a5fb05e build: go 1.26.10, golang.org/x/net v0.62.0
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.
2026-11-03 23:34:39 +01:00
GRAB Racem 2d373ae77a ci: add a race-detector step and shellcheck for the install scripts
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.
2026-11-03 23:30:08 +01:00
GRAB Racem fc08133ab2 fix(docker): install Node 22 in the runtime image
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.
2026-11-03 23:26:50 +01:00
GRAB Racem 334f6fdddc fix(llm): send a saved API key only to the endpoint it was saved for
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.
2026-11-03 23:25:47 +01:00
GRAB Racem a20628d334 fix(config): fail on a malformed config.json and escape install.sh values
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.
2026-11-03 23:22:34 +01:00
GRAB Racem a6e3fd0a2e fix(agent): match ARTEX_LLM_PROVIDER case-insensitively
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.
2026-11-03 23:20:21 +01:00
GRAB Racem ddddddd1f3 fix(backup): reject --keep 0 so the new dump is never pruned
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).
2026-11-03 23:18:45 +01:00
GRAB Racem f3f9b8d4fd fix(gitignore): split the merged bench/backups line so backups/ is ignored
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.
2026-11-03 23:17:56 +01:00
BaaRai 23cf14cf0a Merge pull request #137 from BaaRai/fix/auth-init-setup-token
fix(auth): require a setup token for /api/auth/init and bind 8787 to localhost
2026-11-03 22:22:30 +01:00
BaaRai 5bad722593 fix(auth): make the published bind address configurable
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).
2026-11-03 22:18:27 +01:00
GRAB Racem 2aab3e8210 fix(auth): require a setup token for /api/auth/init and bind 8787 to 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.
2026-11-03 22:18:27 +01:00
BaaRai d45f0c7811 Merge pull request #136 from BaaRai/fix/shell-env-secrets
fix(security): remove startup secrets from the environment before child processes spawn
2026-11-03 22:17:26 +01:00
BaaRai e6f4de5ddc fix(security): scrub startup secrets before any child can spawn
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.
2026-11-03 22:14:35 +01:00
GRAB Racem ac601b7e59 fix(security): remove startup secrets from the environment before child 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.
2026-11-03 21:46:40 +01:00