Maestro task: 176b2a30-6e90-4339-b430-5b8967b2fd32 (Feature 11 - Settings Home and Admin AI Assistant).
Done-condition
GET /api/v1/settings/ai returns provider, base_url, model, temperature, system_prompt, api_key_last4 and key_verified_at; PATCH validates provider against the presets, temperature 0-1 in 0.1 steps, system_prompt <= 8000 chars; PUT /settings/ai/api-key stores the key with secretbox and records last4; POST /settings/ai/verify calls Provider.Verify and stores key_verified_at on success or returns the provider's typed error; GET /settings/ai/models returns the live model list; POST /settings/ai/reset-prompt restores the default persona from CTXD PROMPTS.MD section 4. A test scans every response body and asserts the full key never appears; a harness test with the Fake provider proves a PATCHed model and temperature are used by the next skill run. Go quality floor passes.
Security hardening (second commit)
PATCH /settings/ai refuses to change provider or base_url while a key is stored (400 "remove the key first"), so a stored key can never be re-pointed at an attacker's server.
A key is only accepted for a custom base_url that passes secureurl.Check (https, or plain http only to localhost) - no key over plain http to a LAN host.
Tests cover the endpoint lock, the redirect-with-key case, the secure-URL rule, and the response-body key scan.
Evidence
gofmt -l . empty; go vet ./...; CGO_ENABLED=0 go build ./...; go test -race -count=1 ./... all pass; no new dependency; no sqlc change.
docs/ARCHITECTURE.md updated.
Notes recorded, not acted on: no admin role exists yet (single-role by design); blocking local/cloud-metadata base_urls would break the normal Ollama case, and the key-leak it guarded against is already closed by the endpoint lock.
Maestro task: 176b2a30-6e90-4339-b430-5b8967b2fd32 (Feature 11 - Settings Home and Admin AI Assistant).
## Done-condition
GET /api/v1/settings/ai returns provider, base_url, model, temperature, system_prompt, api_key_last4 and key_verified_at; PATCH validates provider against the presets, temperature 0-1 in 0.1 steps, system_prompt <= 8000 chars; PUT /settings/ai/api-key stores the key with secretbox and records last4; POST /settings/ai/verify calls Provider.Verify and stores key_verified_at on success or returns the provider's typed error; GET /settings/ai/models returns the live model list; POST /settings/ai/reset-prompt restores the default persona from CTXD PROMPTS.MD section 4. A test scans every response body and asserts the full key never appears; a harness test with the Fake provider proves a PATCHed model and temperature are used by the next skill run. Go quality floor passes.
## Security hardening (second commit)
- PATCH /settings/ai refuses to change provider or base_url while a key is stored (400 "remove the key first"), so a stored key can never be re-pointed at an attacker's server.
- A key is only accepted for a custom base_url that passes secureurl.Check (https, or plain http only to localhost) - no key over plain http to a LAN host.
- Tests cover the endpoint lock, the redirect-with-key case, the secure-URL rule, and the response-body key scan.
## Evidence
- gofmt -l . empty; go vet ./...; CGO_ENABLED=0 go build ./...; go test -race -count=1 ./... all pass; no new dependency; no sqlc change.
- docs/ARCHITECTURE.md updated.
Notes recorded, not acted on: no admin role exists yet (single-role by design); blocking local/cloud-metadata base_urls would break the normal Ollama case, and the key-leak it guarded against is already closed by the endpoint lock.
GET/PATCH /api/v1/settings/ai return and change provider, base_url,
model, temperature and system_prompt, with read-only api_key_last4 and
key_verified_at. PATCH requires a preset provider, temperature 0-1 in
0.1 steps and a system_prompt of at most 8000 characters.
PUT /settings/ai/api-key seals the key with secretbox and records its
last four characters; POST /settings/ai/verify calls Provider.Verify and
records key_verified_at or returns the provider's typed error;
GET /settings/ai/models lists the live models; POST
/settings/ai/reset-prompt restores household.DefaultPersonaPrompt.
The new internal/ai/aisettings service also builds the harness.Config
for each skill run from the saved settings. presets.ForAccount builds a
provider's ListModels and Verify without a model, so a key can be
checked and a model picked before one is chosen.
Tests scan every response body from these routes for the full key, and
a harness run with the Fake provider uses the PATCHed model and
temperature.
Maestro task: 176b2a30-6e90-4339-b430-5b8967b2fd32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PATCH /settings/ai refused nothing when the base URL or provider changed
with a key stored, so a session could point base_url at another server
and have POST /settings/ai/verify send it the key. Both changes are now
refused while a key is stored: remove the key, change the endpoint, then
store the new key. A preset without a fixed base URL takes a key only
when its base URL passes secureurl.Check, so the key never crosses the
network in plain text.
Maestro task: 176b2a30-6e90-4339-b430-5b8967b2fd32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET/PATCH /api/v1/settings/ai return and change provider, base_url,
model, temperature and system_prompt, with read-only api_key_last4 and
key_verified_at. PATCH requires a preset provider, temperature 0-1 in
0.1 steps and a system_prompt of at most 8000 characters.
PUT /settings/ai/api-key seals the key with secretbox and records its
last four characters; POST /settings/ai/verify calls Provider.Verify and
records key_verified_at or returns the provider's typed error;
GET /settings/ai/models lists the live models; POST
/settings/ai/reset-prompt restores household.DefaultPersonaPrompt.
The new internal/ai/aisettings service also builds the harness.Config
for each skill run from the saved settings. presets.ForAccount builds a
provider's ListModels and Verify without a model, so a key can be
checked and a model picked before one is chosen.
Tests scan every response body from these routes for the full key, and
a harness run with the Fake provider uses the PATCHed model and
temperature.
Maestro task: 176b2a30-6e90-4339-b430-5b8967b2fd32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PATCH /settings/ai refused nothing when the base URL or provider changed
with a key stored, so a session could point base_url at another server
and have POST /settings/ai/verify send it the key. Both changes are now
refused while a key is stored: remove the key, change the endpoint, then
store the new key. A preset without a fixed base URL takes a key only
when its base URL passes secureurl.Check, so the key never crosses the
network in plain text.
Maestro task: 176b2a30-6e90-4339-b430-5b8967b2fd32
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /settings/ai/models now removes the stored API key from every model
ID and display name the provider returns, so a provider that echoes the
key in a successful list cannot disclose it.
PATCH /settings/ai gets its own body limit, sized for 8,000 characters at
12 bytes of JSON each (a \u-escaped surrogate pair) plus the other
fields. A valid prompt of three- or four-byte characters was refused by
the shared 16 KiB limit; the 8,000-character rule stays the only limit
on the prompt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Maestro task: 176b2a30-6e90-4339-b430-5b8967b2fd32 (Feature 11 - Settings Home and Admin AI Assistant).
Done-condition
GET /api/v1/settings/ai returns provider, base_url, model, temperature, system_prompt, api_key_last4 and key_verified_at; PATCH validates provider against the presets, temperature 0-1 in 0.1 steps, system_prompt <= 8000 chars; PUT /settings/ai/api-key stores the key with secretbox and records last4; POST /settings/ai/verify calls Provider.Verify and stores key_verified_at on success or returns the provider's typed error; GET /settings/ai/models returns the live model list; POST /settings/ai/reset-prompt restores the default persona from CTXD PROMPTS.MD section 4. A test scans every response body and asserts the full key never appears; a harness test with the Fake provider proves a PATCHed model and temperature are used by the next skill run. Go quality floor passes.
Security hardening (second commit)
Evidence
Notes recorded, not acted on: no admin role exists yet (single-role by design); blocking local/cloud-metadata base_urls would break the normal Ollama case, and the key-leak it guarded against is already closed by the endpoint lock.
Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.
Independent Codex gpt-6-sol review: APPROVED for this exact commit.