Phase 3 of secrets at rest (#213, #216), same AES-256-GCM scheme and key.
- tools.exec joins the secret columns for system=false rows (AAD
tools.exec): the whole JSON is sealed in the {"__artex_enc":"enc:v1:..."}
envelope, so headers, body, proxy, script code and command are covered;
{} stays {}
- CreateCustomTool and UpdateCustomTool seal it; scanTool opens it, so
GetTool, ListTools and ListCustomTools (and through them customTools,
the platform tools and the test endpoint) see the clear JSON in memory
- the startup backfill, the no-key refusal and the wrong-key check cover
it, for system=false rows only; built-in rows stay in clear for the seeds
API: GET /api/tools returns the header values, proxy and body of a custom
tool masked, with headers_set, proxy_set and body_set. On update (API and
update_custom_tool) the placeholder keeps the stored value, an explicit
value replaces it and "" clears it; on create (API and create_custom_tool)
a placeholder is refused. The test endpoint takes an optional key: the
saved tool's stored values replace the placeholders; the editor sends it.
Phase 2 of secrets at rest (#213), same AES-256-GCM scheme and key.
- llm_profiles.proxy joins the secret columns (AAD llm_profiles.proxy);
scanProfile decrypts it for both column lists, SaveProfile encrypts
it, an empty proxy stays NULL
- brave_search_api_key, tavily_search_api_key, web_search_proxy and
global_proxy are stored encrypted (AAD settings.<key>) through
GetSetting, SetSetting and InsertSettingIfAbsent; other settings stay
in clear
- the startup backfill, the no-key refusal and the wrong-key check
cover the new values; legacy plaintext is read as-is until backfill
API: the proxies are returned masked as a whole (placeholder plus a
*_set flag) by GET /api/settings, GET /api/llm/profiles and GET /api/llm.
On write the placeholder keeps the stored value, an explicit value
replaces it and "" clears it; the placeholder is never stored. The test
and model-list endpoints resolve the placeholder to the stored proxy.
A masked env value keeps the stored secret only while transport, command, args (canonical JSON) and url are unchanged, as notify.PrepareConfigUpdate does for channels. A changed destination with a masked value is refused (400). The credentials tab drops reveal/copy and shows a stored password as "stored, hidden".
Seal the four credential columns with AES-256-GCM (random nonce per value,
AAD "<table>.<column>"): llm_profiles.api_key and task_credentials.password
store "enc:v1:<base64(nonce||ciphertext)>"; mcp_servers.env and
notification_channels.config store {"__artex_enc":"enc:v1:..."} over the
canonical JSON. The db layer encrypts on write and decrypts on read, so agent
tools (LLM provider, MCP, notifier, test credentials) get plaintext
server-side. Decryption failure is an error, never a plaintext fallback.
Key: ARTEX_SECRET_KEY (32 bytes, base64 or hex) or keys/secret.key next to
jwt.key, generated with crypto/rand at first start (0600). Startup refuses
when encrypted values exist and no key is available, and when the key does
not decrypt them. An idempotent backfill under the schema advisory lock
encrypts legacy plaintext rows and logs only a count.
API: MCP env is masked on read and masked placeholders on write keep the
stored value; task credential passwords are masked server-side and a masked
password on write is refused. ARTEX_SECRET_KEY is scrubbed from the process
environment with the other startup secrets.
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.
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.
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.
Descriptions written through the API were interpolated raw into the SKILL.md
frontmatter, so a value containing a colon+space made the whole frontmatter
unparseable: every field silently stayed empty, the skill loaded under its
directory name, and a skill-gated MCP server was silently skipped. The same
applied to updates through the meta rewrite, and the shipped scopesentry skill
had a malformed name line all along.
- yamlScalar: plain-safe tokens stay bare, everything else single-quoted
- fsCreateSkill/fsUpdateSkillMeta emit safe values and re-verify the written
file with the real skill parser (name + mcps must survive), rejecting the
write otherwise
- warnBrokenSkills at startup logs skills whose name fell back to the
directory (the signature of broken frontmatter)
- fix skills/scopesentry/SKILL.md name line
Closes#39
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.
Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
full text search) now use ASCII equivalents; go build passes and the
test suite matches the pre translation baseline (remaining failures
need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
matching the backend, chat mentions tests pass