@@ -15,12 +15,18 @@ has its own `httpapi/apple.go` — see `NEXT.md` Tier 1).
1515Tier 2 added one admin endpoint on top of those, the first in this repo
1616— see below. Tier 3 added three more admin endpoints and this repo's
1717first three tables of its own, plus the config that lights up Argon2id,
18- cloud logging and email templates — see below. Tier 4's Stage 1 added
19- three more admin endpoints and this repo's fourth table — the weekly
18+ cloud logging and email templates — see below. Tier 4 added six more
19+ admin endpoints and two more tables of its own: Stage 1 is the weekly
2020digest and its recorded history, the support-ticket login diagnosis and
21- the config tuning advisor. Every admin endpoint in this repo is either
22- read-only or an explicit operator action on a named key; nothing on that
23- surface applies a suggestion by itself.
21+ the config tuning advisor; Stage 2 is the AI provider settings — the LLM
22+ provider, the read-only database and the ask-ai widget config. Tier 4 is
23+ also where this repo stopped being purely a wrapper: it now ships a live
24+ ` ai.LLMProvider ` over the Anthropic SDK and a live ` ai.QueryableStore `
25+ over a second database connection, neither of which is wired to a
26+ consumer yet. Every admin endpoint here is read-only except the
27+ ` /v1/admin/settings/* ` saves, which are the human half of the
28+ pre-fill-never-auto-apply rule — nothing on that surface applies a
29+ suggestion by itself.
2430
2531Tier 1 also added the second-factor surface: TOTP enroll/confirm/
2632disable, passkey registration/list/delete, magic-link request/complete,
@@ -308,15 +314,19 @@ in-memory double, not against Postgres `FOR UPDATE SKIP LOCKED`, and
308314that double cannot reproduce two workers racing. ` PROGRESS.md ` says all
309315of this plainly.
310316
311- ## Tier 4 — AI-assisted admin endpoints (Stage 1) : DONE
317+ ## Tier 4 — AI-assisted admin endpoints: DONE
312318
313319Built in two stages on ` feat/tier4-ai-admin-endpoints ` , for the same
314320reason Tier 3 was: the three read-only reports below had their decisions
315- already made in ` NEXT.md ` , while Stage 2 needs two decisions that are
316- not a build session's to make (see the end of this section). ` go build ` ,
317- ` go vet ` , ` gofmt -l ` and ` go test ./... ` are clean, ` httpapi ` is also
318- green under ` -race ` , and ` PROGRESS.md ` records what that does and does
319- not cover.
321+ already made in ` NEXT.md ` , while Stage 2 needed two decisions that are
322+ not a build session's to make. Those two were resolved by following
323+ ` NEXT.md ` 's own instruction to make the reasonable call and record it —
324+ see Stage 2 below. ` go build ` , ` go vet ` , ` gofmt -l ` and ` go test ./... `
325+ are clean, ` httpapi ` and the two new packages are also green under
326+ ` -race ` , and ` PROGRESS.md ` records what that does and does not cover,
327+ which is a lot.
328+
329+ ### Stage 1 — the three read-only reports
320330
321331Everything here is ` RequireAdmin ` , read-only, and buildable on the
322332engine alone — no LLM, no second database connection, no outbound call:
@@ -373,19 +383,89 @@ sandbox — and the digest schedule is a goroutine on
373383` context.Background() ` , because this repo still has no graceful
374384shutdown. ` PROGRESS.md ` says both plainly.
375385
376- ## Tier 4 Stage 2, and Tier 5
386+ ### Stage 2 — the providers and the widget config
387+
388+ This is the half of the tier that needed an LLM, a second database
389+ connection and an outbound call, so it is the half where this repo
390+ stopped being purely a wrapper. Three settings endpoints, all
391+ ` RequireAdmin ` , all in ` httpapi/settings_handlers.go ` , backed by
392+ ` settings/ ` and ` migrations/013_settings ` :
393+
394+ - ** ` GET ` /` PUT ` /` DELETE /v1/admin/settings/llm-provider ` ** stores which
395+ model and key back ` ai.LLMProvider ` . ` DELETE ` was added alongside the
396+ specced pair: a settings screen with no way to clear a credential is
397+ a screen an operator cannot leave.
398+ - ** ` GET ` /` PUT ` /` DELETE /v1/admin/settings/database-provider ` ** stores
399+ the connection ` ai.QueryableStore ` runs against.
400+ - ** ` GET ` /` PUT ` /` DELETE /v1/admin/settings/ask-ai-widget ` ** stores the
401+ widget's enabled flag, allowed origins, entity scope and copy.
402+
403+ Both credentials are sealed with ** AES-256-GCM before they reach the
404+ table** , keyed from a new ` SETTINGS_ENCRYPTION_KEY ` . That key is
405+ deliberately * not* cryden's ` ENCRYPTION_KEY ` : the two seal different
406+ things with different lifetimes and blast radii, and this repo already
407+ sets the precedent with ` CLOUD_LOG_HASH_KEY ` . An unset key is not a
408+ startup failure — the three endpoints answer ` 404 not_configured ` , like
409+ every other optional feature here. The encryption itself is cryden's
410+ ` security.NewAESGCMEncryptor ` rather than a second implementation of the
411+ same primitive; see ` settings/secrets.go ` .
412+
413+ Three things in this stage are worth reading before touching them:
414+
415+ - ** ` PUT /database-provider ` proves the role cannot write, then stores.**
416+ Order is the whole design: validate the shape, connect with the
417+ supplied credentials and attempt a write, and only store once the
418+ server refuses. The probe targets ` pg_temp ` , so a failed probe leaves
419+ nothing behind, and the pool is pinned to one connection so the
420+ ` CREATE ` and the ` INSERT ` share the session owning that temp table.
421+ Three outcomes are distinguished — refused is a pass, succeeded is
422+ ` 400 database_role_not_read_only ` , anything else is
423+ ` 400 database_role_unverified ` and ** is not a pass** . Only SQLSTATE
424+ ` 42501 ` counts as a refusal, matched by code rather than message.
425+ - ** ` aiprovider.ScopedProvider ` gives the widget's ` entities ` setting
426+ teeth.** cryden's ` widget.Ask ` force-scopes every parsed intent to the
427+ calling end user's own rows, overwriting whatever identity filter the
428+ model produced rather than validating it — no oracle — but it does so
429+ over all of ` ai.AllowedEntities ` . Narrowing that is a host decision, so
430+ this repo refuses an out-of-scope entity in front of the provider.
431+ - ** ` settings.AskAIWidgetConfig ` is not a credential** , and that is why
432+ it has no ` Redacted ` counterpart while the other two do. All three are
433+ stored through the same ` Secrets ` wrapper anyway — one storage path
434+ with one rule about what reaches the table is worth more than saving a
435+ decryption.
436+
437+ ` aiprovider.NewAnthropic ` is a real ` ai.LLMProvider ` over the official
438+ Anthropic Go SDK, and ` aiprovider.NewPostgresSnapshot ` a real
439+ ` ai.QueryableStore ` . ** Nothing wires either from the stored config yet** :
440+ the only consumer would be a widget serving endpoint, which does not
441+ exist, so that glue lands with its first caller rather than being
442+ written blind. ` allowed_origins ` is stored and validated but nothing
443+ consults it at request time for the same reason, and the widget GET
444+ carries no embed snippet because the URL in one would name a route this
445+ repo does not serve.
446+
447+ What Stage 2 does ** not** have evidence for, and ` PROGRESS.md ` says in
448+ full: ` CheckReadOnly ` has never run against a real Postgres (the tested
449+ branch is the * unverifiable* one, not the pass), the Anthropic provider
450+ has never called Anthropic (it is tested against a local fake in the
451+ Messages API's wire shape), and ` 013_settings ` has never been applied to
452+ a database.
453+
454+ ** The read-only rule now has a named exception, and it is this one.**
455+ ` /v1/admin/settings/* ` is the admin surface's first write. The reading
456+ is that ` CLAUDE.md ` 's rule covers the AI * tools* — which cryden builds
457+ through interfaces carrying no way to act — rather than every route
458+ under ` /v1/admin ` , and that a settings save is exactly what ` NEXT.md ` 's
459+ pre-fill-never-auto-apply decision names as the human half. No
460+ AI-assisted handler holds a reference to these routes, and none accepts
461+ a suggestion as input. The alternative readings (store the key in
462+ cryden, or environment-only) are worse and one of them is explicitly
463+ ruled out by ` NEXT.md ` , which says this repo owns that config storage.
464+
465+ ## Tier 5
377466
378467Not started. See ` NEXT.md ` for the full, ordered, specced-in-detail
379- queue. Stage 2 is the LLM provider config, the read-only database
380- provider config and the ask-AI widget config;
381- ` ai.LLMProvider ` /` ai.QueryableStore ` have no implementation in this repo
382- yet, so nothing in it has an endpoint. Two decisions are open and were
383- left for the user rather than guessed at: whether this repo ships a live
384- LLM client against a real vendor (an outbound integration, which this
385- repo has so far shipped none of), and where the at-rest encryption key
386- for the stored provider credential comes from (reusing the existing
387- ` ENCRYPTION_KEY ` is the obvious candidate and still a decision with a
388- blast radius). Tier 4 stays read-only by construction, with the
389- decision already made that an AI suggestion ** pre-fills** a settings
390- form and never auto-applies.
468+ queue — the users admin surface, which has no engine gap and is just
469+ missing endpoints, plus the widget's own serving endpoint, which is what
470+ the Stage 2 config above is waiting for.
391471
0 commit comments