Compare commits

..
Author SHA1 Message Date
GRAB Racem 1a49abaf09 fix(docker): build the image from source so a fresh clone builds
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 00:26:39 +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 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
14 changed files with 248 additions and 65 deletions
+20
View File
@@ -44,6 +44,26 @@ jobs:
# as rotating failures in the agent package). Serial package execution
# keeps the database state deterministic.
run: go test -p 1 ./...
- name: go test -race
# Race detector on the packages that pass under -race today. ./server is
# left out until it has been checked with -race against postgres.
# Its own database: the plain run above leaves rows behind and the db tests
# are not idempotent across runs (duplicate llm_profiles names).
env:
ARTEX_PG_DSN: postgres://artex:testpass@localhost:5432/artextest_race?sslmode=disable
run: >-
go test -race -p 1 ./agent ./cmd/artex ./config ./db ./evidence ./guard
./intercept ./llmpool ./llmrec ./mcphttp ./notify ./sidequestion ./traffic
shell:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# shellcheck is preinstalled on the ubuntu runners. -S warning: the scripts
# currently have only info-level notes (intentional A && B || C chains in
# update.sh, an unquoted glob in backup.sh's prune), so fail on warnings.
- run: shellcheck -S warning install.sh update.sh start.sh scripts/backup.sh
web:
runs-on: ubuntu-latest
+4 -1
View File
@@ -57,7 +57,10 @@ Thumbs.db
# bench benchmark related (local artifacts, not committed)
/bench/
/Dockerfile.bench
/Dockerfile.bench.v2backups/
/Dockerfile.bench.v2
# scripts/backup.sh output (database dumps, data archives)
backups/
# posture probe raw output
posture-runs/
+48 -14
View File
@@ -1,24 +1,58 @@
# syntax=docker/dockerfile:1
#
# Runtime image (no compilation inside the image): install common tools only and ship
# a **pre-built Linux single binary**. The binary is cross-compiled by the CI binaries
# job (pure Go, no QEMU) and placed at dist/<TARGETARCH>/artex in the build context.
# This way multi-arch builds only need to emulate the apt layer for arm64 instead of
# emulating the Next/Go compilation, which is much faster.
# Multi-stage build: `docker compose up -d --build` (or `docker build .`) produces
# the complete image from source. No prebuilt binary in the build context is needed,
# so a fresh `git clone` builds — dist/ is git-ignored and is no longer required.
#
# To build the image manually, prepare the binary yourself first:
# cd web && npm run build:static && cd ..
# cp -r web/out server/webui/dist
# CGO_ENABLED=0 GOARCH=amd64 go build -tags embedui -o dist/amd64/artex ./cmd/artex
# docker build -t artex:local .
# frontend (web) -> Go binary with embedded UI (build) -> runtime image
#
# The Go binary is produced by build.sh with the same flags the release workflow's
# binaries job uses, so the image binary and the published release binary come from
# one source.
# ---------------------------------------------------------------- frontend ---
# Pinned to the Node 22 line the CI/release workflows use. On the build platform
# only; the frontend static export is architecture-independent.
FROM --platform=$BUILDPLATFORM node:22-bookworm-slim AS web
WORKDIR /src/web
COPY web/package.json web/package-lock.json ./
RUN npm ci --ignore-scripts
COPY web/ ./
RUN npm run build:static
# ------------------------------------------------------------- Go binary ---
# Pinned to the Go 1.26 line (matches go.mod and the release binaries job). The
# binary is CGO-free, so this stage runs on the build platform and cross-compiles
# to $TARGETARCH without QEMU.
FROM --platform=$BUILDPLATFORM golang:1.26-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Reuse the frontend built above; build.sh skips its own frontend step.
COPY --from=web /src/web/out ./server/webui/dist
ARG ARTEX_VERSION=dev
ARG TARGETOS
ARG TARGETARCH
# Same flags as the release binaries job (ARTEX_COMPRESS=0, -trimpath,
# -X main.version), driven through build.sh so there is one build path.
RUN ARTEX_SKIP_FRONTEND=1 \
ARTEX_SKIP_NPM_CI=1 \
ARTEX_COMPRESS=0 \
ARTEX_OUTPUT=/out/artex \
ARTEX_BUILD_VERSION="${ARTEX_VERSION}" \
bash ./build.sh --target "${TARGETOS:-linux}/${TARGETARCH:-amd64}"
# ---------------------------------------------------------------- runtime ---
FROM python:3.12-slim-bookworm
ARG TARGETARCH
# Common tools: ripgrep / curl / vim, plus a set of recon staples (add/remove as needed).
# Node 20.x comes from NodeSource: bookworm's apt nodejs is 18, but Playwright requires >=20.
# Node 22.x comes from NodeSource: bookworm's apt nodejs is 18, Playwright requires >=20,
# and 20 is end-of-life. 22 matches the CI and release workflows.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates ripgrep curl wget vim git jq unzip \
dnsutils iputils-ping netcat-openbsd inetutils-telnet whois nmap \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Web-recon toolbox (drost-community take 1, #100): pinned release binaries installed
@@ -83,8 +117,8 @@ RUN npm install -g @playwright/mcp@latest @playwright/cli@latest playwright@late
&& playwright install --with-deps chromium \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# pre-built binary for the matching architecture (dist/amd64/artex or dist/arm64/artex)
COPY dist/${TARGETARCH}/artex /app/artex
# Binary compiled from source in the build stage above.
COPY --from=build /out/artex /app/artex
# Daemon startup script: relaunches the process after a crash and forwards SIGTERM
# to artex — docker stop only signals PID 1; without forwarding, artex never receives it,
# cannot shut down gracefully, and is hard-killed by SIGKILL after 10 seconds.
+3 -1
View File
@@ -203,7 +203,9 @@ func compactionConfig(windowTokens int) *compaction.Config {
// ARTEX_LLM_PROXY = proxy URL (optional; http/https/socks5)
// ANTHROPIC_API_KEY / OPENAI_API_KEY = credentials (unused for ollama)
func FromEnv() (Config, bool) {
prov := os.Getenv("ARTEX_LLM_PROVIDER")
// Case- and space-insensitive: "Ollama" used to miss every case below and
// fall through to anthropic, sending ANTHROPIC_API_KEY to the Ollama base URL.
prov := strings.ToLower(strings.TrimSpace(os.Getenv("ARTEX_LLM_PROVIDER")))
anthKey := os.Getenv("ANTHROPIC_API_KEY")
oaiKey := os.Getenv("OPENAI_API_KEY")
+37
View File
@@ -0,0 +1,37 @@
package agent
import (
"testing"
"github.com/Autumn-27/norma/llm"
)
// ARTEX_LLM_PROVIDER is matched regardless of case and surrounding spaces. A
// miss used to fall through to anthropic: "Ollama" then sent ANTHROPIC_API_KEY
// to the Ollama base URL.
func TestFromEnvProviderIsCaseInsensitive(t *testing.T) {
t.Setenv("ANTHROPIC_API_KEY", "sk-ant-secret")
t.Setenv("OPENAI_API_KEY", "sk-oai")
t.Setenv("ARTEX_LLM_BASE_URL", "")
t.Setenv("ARTEX_LLM_MODEL", "")
for _, tc := range []struct {
env string
want llm.Format
key string
}{
{"Ollama", FormatOllama, ""},
{" OLLAMA ", FormatOllama, ""},
{"OpenAI", llm.FormatOpenAI, "sk-oai"},
{"OpenAI-Responses", llm.FormatOpenAIResponses, "sk-oai"},
{"Anthropic", llm.FormatAnthropic, "sk-ant-secret"},
} {
t.Setenv("ARTEX_LLM_PROVIDER", tc.env)
c, ok := FromEnv()
if !ok {
t.Fatalf("%q: FromEnv returned ok=false", tc.env)
}
if c.Format != tc.want || c.APIKey != tc.key {
t.Fatalf("%q: format=%v key=%q, want format=%v key=%q", tc.env, c.Format, c.APIKey, tc.want, tc.key)
}
}
}
+5 -1
View File
@@ -88,7 +88,11 @@ func run() int {
}
defer mgr.Close()
skillDir := config.SkillDir()
skillDir, err := config.SkillDir()
if err != nil {
log.Printf("[config] %v", err)
return 1
}
if abs, err := filepath.Abs(skillDir); err == nil {
skillDir = abs
}
+33 -14
View File
@@ -4,7 +4,9 @@ package config
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"net/url"
"os"
"path/filepath"
@@ -96,16 +98,23 @@ func Path() string {
return candidates[0]
}
// Load reads and parses the config file. A missing/unreadable file yields a zero
// Config (so callers fall back to defaults) rather than an error.
func Load() Config {
// Load reads and parses the config file. A missing file yields a zero Config (so
// callers fall back to defaults). A file that exists but cannot be read or parsed
// is an error: ignoring it would run with defaults the operator did not choose.
func Load() (Config, error) {
var c Config
b, err := os.ReadFile(Path())
if err != nil {
return c
path := Path()
b, err := os.ReadFile(path)
if errors.Is(err, fs.ErrNotExist) {
return c, nil
}
_ = json.Unmarshal(b, &c)
return c
if err != nil {
return c, fmt.Errorf("read config file %s: %w", path, err)
}
if err := json.Unmarshal(b, &c); err != nil {
return Config{}, fmt.Errorf("parse config file %s: %w", path, err)
}
return c, nil
}
// SkillDir returns the skill root directory with precedence:
@@ -113,17 +122,23 @@ func Load() Config {
// env ARTEX_SKILL_DIR > config file (skill_dir) > BaseDir()/skills
//
// The directory is created if it does not exist.
func SkillDir() string {
func SkillDir() (string, error) {
var d string
if v := strings.TrimSpace(os.Getenv("ARTEX_SKILL_DIR")); v != "" {
d = v
} else if v := strings.TrimSpace(Load().SkillDir); v != "" {
d = v
} else {
d = filepath.Join(BaseDir(), "skills")
c, err := Load()
if err != nil {
return "", err
}
if v := strings.TrimSpace(c.SkillDir); v != "" {
d = v
} else {
d = filepath.Join(BaseDir(), "skills")
}
}
_ = os.MkdirAll(d, 0o755)
return d
return d, nil
}
// PostgresDSN resolves the connection string with precedence:
@@ -138,7 +153,11 @@ func PostgresDSN() (dsn, source string, err error) {
if v := strings.TrimSpace(os.Getenv("ARTEX_PG_DSN")); v != "" {
return v, "environment variable ARTEX_PG_DSN", nil
}
db := Load().Database
c, err := Load()
if err != nil {
return "", "", err
}
db := c.Database
if d := strings.TrimSpace(db.DSN); d != "" {
return d, "config file " + Path() + " (database.dsn)", nil
}
+22
View File
@@ -3,6 +3,7 @@ package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
@@ -40,3 +41,24 @@ func TestPostgresDSNPrecedence(t *testing.T) {
t.Fatalf("file dsn verbatim, got %q err %v", got, err)
}
}
// A config file that exists but is not valid JSON must fail loudly, not run with
// defaults (or report "no database config") as if the file were absent.
func TestMalformedConfigIsAnError(t *testing.T) {
cfgPath := filepath.Join(t.TempDir(), "config.json")
// What install.sh used to write for a password containing a double quote.
os.WriteFile(cfgPath, []byte(`{"database":{"host":"h","password":"a"b","dbname":"d"},"skill_dir":"/s"}`), 0o644)
t.Setenv("ARTEX_CONFIG", cfgPath)
t.Setenv("ARTEX_PG_DSN", "")
t.Setenv("ARTEX_SKILL_DIR", "")
if _, _, err := PostgresDSN(); err == nil || !strings.Contains(err.Error(), "parse config file") {
t.Fatalf("PostgresDSN: want a parse error, got %v", err)
}
if d, err := SkillDir(); err == nil {
t.Fatalf("SkillDir: want a parse error, got dir %q", d)
}
if _, err := Load(); err == nil {
t.Fatal("Load: want a parse error")
}
}
+8 -8
View File
@@ -1,6 +1,6 @@
module github.com/Autumn-27/artex
go 1.26.3
go 1.26.10
require (
github.com/Autumn-27/norma v0.4.3
@@ -11,9 +11,9 @@ require (
github.com/lqqyt2423/go-mitmproxy v1.9.2
github.com/miekg/dns v1.1.72
github.com/projectdiscovery/dnsx v1.2.3
golang.org/x/crypto v0.55.0
golang.org/x/net v0.58.0
golang.org/x/text v0.41.0
golang.org/x/crypto v0.58.0
golang.org/x/net v0.62.0
golang.org/x/text v0.43.0
modernc.org/sqlite v1.52.0
)
@@ -51,10 +51,10 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/mod v0.40.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/tools v0.49.0 // indirect
golang.org/x/mod v0.41.0 // indirect
golang.org/x/sync v0.24.0 // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/tools v0.51.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
+14 -14
View File
@@ -106,23 +106,23 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+19 -11
View File
@@ -9,6 +9,16 @@ warn(){ printf '\033[33m[!]\033[0m %s\n' "$*"; }
die(){ printf '\033[31m[x]\033[0m %s\n' "$*" >&2; exit 1; }
ask(){ local p="$1" d="${2:-}" a; read -rp "$p${d:+ [$d]}: " a; echo "${a:-$d}"; }
rand(){ head -c 18 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 24; }
# json_str prints $1 as a JSON string literal: backslash, double quote and the
# control characters read -r can return are escaped.
json_str(){
local s="$1"
s="${s//\\/\\\\}"; s="${s//\"/\\\"}"
s="${s//$'\t'/\\t}"; s="${s//$'\r'/\\r}"
printf '"%s"' "$s"
}
# sed_repl escapes $1 for the replacement side of s|...|...|: \, | and &.
sed_repl(){ printf '%s' "$1" | sed 's/[\\|&]/\\&/g'; }
# ── docker environment check / auto-install ───────────────────
ensure_docker(){
@@ -38,17 +48,14 @@ install_docker(){
local pw key
pw="$(ask 'Postgres password (press Enter to generate randomly)' "$(rand)")"
key="$(ask 'ANTHROPIC_API_KEY (may be empty, configure later in the UI)' '')"
sed -i.bak "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=${pw}|" .env
sed -i.bak "s|^ANTHROPIC_API_KEY=.*|ANTHROPIC_API_KEY=${key}|" .env
sed -i.bak "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(sed_repl "$pw")|" .env
sed -i.bak "s|^ANTHROPIC_API_KEY=.*|ANTHROPIC_API_KEY=$(sed_repl "$key")|" .env
rm -f .env.bak
ok ".env generated (POSTGRES_PASSWORD set)"
else
info "reusing existing .env"
fi
info "building the image from this checkout and starting..."
if [ ! -f "dist/amd64/artex" ] && [ ! -f "dist/arm64/artex" ]; then
warn "no prebuilt binary under dist/ - build it first (frontend, then go build -tags embedui); see the Dockerfile header"
fi
docker compose up -d --build
ok "started → http://localhost:8787"
info "view logs: docker compose logs -f artex"
@@ -76,16 +83,17 @@ install_local(){
DB_SSL="$(ask 'sslmode (disable/require)' disable)" ;;
esac
# generate config.json
# generate config.json (string values JSON-escaped: a password may contain " or \)
case "$DB_PORT" in (*[!0-9]*|'') die "port must be a number: $DB_PORT" ;; esac
cat > config.json <<JSON
{
"database": {
"host": "${DB_HOST}",
"host": $(json_str "$DB_HOST"),
"port": ${DB_PORT},
"user": "${DB_USER}",
"password": "${DB_PASS}",
"dbname": "${DB_NAME}",
"sslmode": "${DB_SSL}"
"user": $(json_str "$DB_USER"),
"password": $(json_str "$DB_PASS"),
"dbname": $(json_str "$DB_NAME"),
"sslmode": $(json_str "$DB_SSL")
}
}
JSON
+3 -1
View File
@@ -31,7 +31,9 @@ while [ $# -gt 0 ]; do
*) echo "unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
done
case "$KEEP" in (*[!0-9]*|'') echo "--keep must be a positive integer" >&2; exit 2 ;; esac
# 0* rejects 0, which would make prune delete the dump this run just wrote, and
# leading zeros, which $((KEEP + 1)) would read as octal.
case "$KEEP" in (*[!0-9]*|''|0*) echo "--keep must be a positive integer" >&2; exit 2 ;; esac
# Compose reads .env for credentials; source it for the dump command too.
if [ -f .env ]; then
+29
View File
@@ -1,6 +1,8 @@
package server
import (
"context"
"os"
"os/exec"
"strings"
"testing"
@@ -34,3 +36,30 @@ func TestScrubSecretEnvHidesSecretsFromChildren(t *testing.T) {
t.Errorf("non-secret variable did not reach the child process")
}
}
// New must apply the options it receives. An earlier version accepted
// ...Option and never called them, so the binary kept the secrets in the
// environment while the unit test above still passed.
func TestNewAppliesSecretScrubOption(t *testing.T) {
// t.Setenv restores both after the test: the scrub unsets them for the rest
// of this process, and later tests still need the DSN.
if dsn := os.Getenv("ARTEX_PG_DSN"); dsn != "" {
t.Setenv("ARTEX_PG_DSN", dsn)
}
t.Setenv("ANTHROPIC_API_KEY", "scrub-probe")
t.Setenv("OPENAI_API_KEY", "scrub-probe")
m, err := NewManager(t.TempDir(), "")
if err != nil {
t.Skipf("no manager: %v", err)
}
t.Cleanup(func() { m.Close() })
ctx, cancel := context.WithCancel(context.Background())
cancel()
_ = New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir(), WithSecretScrub())
if os.Getenv("ANTHROPIC_API_KEY") != "" || os.Getenv("OPENAI_API_KEY") != "" {
t.Fatal("New did not apply WithSecretScrub: the secrets are still in the environment")
}
}
+3
View File
@@ -156,6 +156,9 @@ func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDi
profChatAgents: map[int64]*agent.ChatAgent{},
provByProfile: map[int64]*provEntry{}, llmHealth: newLLMHealthRegistry(m.pg),
taskAgents: map[string]*taskAgentBundle{}, archiveWake: make(chan struct{}, 1)}
for _, o := range opts {
o(s)
}
s.initSetupToken()
s.initSideQuestions()
// Circuit-breaker thresholds/cooldowns are hot parameters on the failure path; push the global retry policy to the Registry once at startup,