49 Commits
Author SHA1 Message Date
k.renner fbd5a2529a Merge pull request 'fix(auth): the wiki-index BasicAuth credentials read the API again' (#15) from feat/basic-auth-machines into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
Reviewed-on: #15
2026-10-19 14:13:52 +00:00
hermes-bot 50fb31b950 fix(auth): the wiki-index BasicAuth credentials read the API again
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
#14 put the read API behind a session or an API key. External agents
reach the wiki through wiki-index.renner.uno with BasicAuth only, and
since #14 they got 401: a regression.

The service now checks `Authorization: Basic` itself against the same
htpasswd lines Traefik's wiki-api-basic-auth middleware uses
(Auth:Basic:UsersPath: a file, or a mounted Secret directory where every
visible key is read, as Traefik reads every key of an Opaque Secret).
This is a credential check in the service, not trust in a proxy: the
header proves nothing unless the password matches the hash.

- Hash formats Traefik accepts: bcrypt ($2a$/$2b$/$2y$, BCrypt.Net-Next
  4.2.1), Apache MD5 ($apr1$, implemented here and checked against the
  Traefik docs example and `openssl passwd -apr1`), SHA1 ({SHA}). Any
  other format authenticates nobody.
- The identity is a machine named after the BasicAuth user.
- Policy "read" accepts it; "write" stays API-key only, and the UI stays
  OIDC only.
- A verified (user, password, hash) triple is cached in memory, so bcrypt
  costs once per process.
- With no UsersPath configured the scheme authenticates nobody, so this
  image is safe before the deployment mounts the Secret.

Tests: all three hash formats read the API; a wrong password, an unknown
user, an empty password and a user found only in a hidden Secret entry
are 401; BasicAuth cannot write and does not open the UI.
2026-10-19 14:06:55 +00:00
k.renner 311f59d82a Merge pull request 'feat(auth): humans sign in with OIDC; no header names a user' (#14) from feat/oidc-login into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
Reviewed-on: #14
2026-10-19 10:13:28 +00:00
hermes-bot 4820bc9f6d fix(auth): redirect URI from the request host, no PublicOrigin setting
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Auth:Oidc:PublicOrigin was a hand copy of the Ingress host. Traefik routes on
the Host header and Authentik matches redirect URIs strictly, so the URI is
https://<request host>/signin-oidc; https because TLS ends at Traefik and
browsers only reach the wiki over https.
2026-10-19 09:45:56 +00:00
hermes-bot 0ecf1aaac5 feat(auth): humans sign in with OIDC; no header names a user
ci/woodpecker/push/test Pipeline was successful
The ProxyHeader scheme took the username from X-Authentik-Username for any
peer inside Auth:ReverseProxy:TrustedProxies. The deployment set that to
the whole pod CIDR (a hand copy of the Talos podSubnets), so every pod the
network policies let reach :8080 (Hermes, Gitea, Traefik, all of ai and
monitoring) could claim any human's name on the Facts page and on saved
answers. Authentik's proxy provider sends no signed identity header, so the
header cannot be verified at all.

Humans now sign in with OpenID Connect against Authentik: a public client
with PKCE, the username is preferred_username from the ID token, whose
signature is checked against the provider's JWKS. Nothing about the
network path or the request headers names a user any more.

- Auth:Oidc:Authority, ClientId and PublicOrigin are required; the host
  refuses to start without them. PublicOrigin builds the redirect URI,
  because TLS ends at Traefik and the request scheme is http.
- UI pages, static assets and the Blazor hub need a session (fallback
  policy); an anonymous browser is redirected to the provider.
- The read API (/api/v1 reads, /ask, /questions, /rank, /status) needs a
  session or an API key (policy "read") and answers 401, never a redirect.
  Before, it was anonymous and relied on the gate, BasicAuth or network
  policy in front.
- Writes still need an API key (policy "write").
- /health, /ready and the HMAC-signed /webhook stay anonymous.
- ProxyHeaderAuthenticationHandler and Auth:ReverseProxy are removed.

Tests: an in-process OpenID provider issues RS256 ID tokens. They cover
the PKCE challenge and redirect URI, the full code flow ending in a
session named after the token's user, rejection of a token signed by a
foreign key, the Authentik header signing in nobody, and 401s for
credential-less API calls.
2026-10-19 09:44:58 +00:00
k.renner 53c03a8e53 Merge pull request 'feat(facts): pictures as part of a statement, kept next to the raw source' (#13) from feat/fact-images into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
ci/woodpecker/manual/test Pipeline was successful
Reviewed-on: #13
2026-09-25 07:55:43 +00:00
claude-botandClaude Fable 5.1 c94afb004c feat(facts): pictures as part of a statement, kept next to the raw source
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
A statement may carry images (a type label, for instance). The drafter
sees them as image blocks and transcribes what is printed; the deposit
stores each picture beside the note as raw/<stem>-<n>.<ext>, lists it in
the frontmatter and embeds it in the body. INoteStore gains a binary
create and a browse URL, so the raw page can link to the pictures. The
page takes pictures from a file picker (camera on phones) or from paste,
resized in the browser to 1600px.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 09:52:16 +02:00
k.renner 125911a4e6 Merge pull request 'feat(facts): one text box; the drafter turns a statement into a raw source' (#12) from feat/fact-from-statement into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
Reviewed-on: #12
2026-09-25 06:57:04 +00:00
claude-botandClaude Fable 5.1 8b8e56d1b2 feat(facts): one text box; the drafter turns a statement into a raw source
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
IFactDrafter port: a statement in, title/content/tags out. Anthropic
implements it over the transport the answer model already uses (extracted
into MessagesTransport). RecordStatementUseCase drafts, sets the person as
source, keeps the statement verbatim in the body and deposits through the
existing use case. The Facts page is the text box and the receipt; the API
shape for machines is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 08:55:45 +02:00
k.renner e0a0cf3085 Merge pull request 'feat(ui): Facts page deposits a raw source under the signed-in user' (#11) from feat/facts-page into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
Reviewed-on: #11
2026-09-25 06:26:41 +00:00
claude-botandClaude Fable 5.1 09ec2cf1d9 feat(ui): Facts page deposits a raw source under the signed-in user
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Same DepositFactUseCase as POST /api/v1/facts, attributed as a human
identity from the Authentik proxy header. Cascading authentication state
carries the principal into the Blazor circuit; without a signed-in user
the form is replaced by a notice. Lists the raw sources awaiting the
nightly distill.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 08:24:00 +02:00
k.renner 4cd8ef1dd4 Merge pull request 'fix(ask): judge every question in one request' (#10) from fix/judge-one-request into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
Reviewed-on: #10
2026-09-25 06:01:55 +00:00
claude-botandClaude Fable 5.1 347ac9d7a6 fix(ask): judge every question in one request
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
The state dominates a System One request and each question is scored in
isolation against it, so sending context choices separately only re-sent
the state per request and added round trips. A live ask carried 22,957
input tokens over two requests for one question; one request carries it
once. Drops the now unused AskOptions.MaxParallel.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 08:00:44 +02:00
k.renner 8fa9f20192 Merge pull request 'Phase 6: rank evaluation (POST /api/v1/lab/eval) and Lab page' (#8) from feat/lab into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
Reviewed-on: #8
2026-09-24 14:57:42 +00:00
k.renner f6a2d875e6 Merge pull request 'Phase 5: questions endpoint, Anthropic answer model, Answers page' (#7) from feat/answer-model into main
ci/woodpecker/push/release Pipeline is pending
ci/woodpecker/push/test Pipeline was canceled
Reviewed-on: #7
2026-09-24 14:57:29 +00:00
claude-botandClaude Fable 5.1 707c5f50a7 Merge branch 'feat/answer-model' into feat/lab
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
Both phases register their use cases and map their endpoints in the same host
files. The lab host test asserted exactly one labelled query, which held only
while no other test in the shared fixture wrote to output/; it now checks the
seeded query is among the rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 16:53:49 +02:00
k.renner 49e4c9893f Merge pull request 'Phase 4: POST /api/v1/facts' (#6) from feat/facts into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
Reviewed-on: #6
2026-09-24 14:51:04 +00:00
claude-botandClaude Fable 5.1 e28a403284 docs: list the lab eval endpoint
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 16:43:17 +02:00
claude-botandClaude Fable 5.1 e6f5c80a3a feat(questions): answer questions from wiki evidence with an answer model
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
POST /api/v1/questions runs ask (rank + judge), selects the paragraphs that
carry probability mass, and streams one answer-model call over them. Below
the coverage gate no model is called and the response names the gap. With
save: true (signed-in user or API key) the answer lands in output/ as a
synth-shaped document with sources, confidence and the answering model.

The Answers page streams the same use case and lists saved answers.
Anthropic is the first IAnswerModel adapter; the answer model is chosen per
request or from Wiki:AnswerModels:Active.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 16:42:18 +02:00
claude-botandClaude Sonnet 5 23c891db8f feat(lab): rank evaluation use case, API and Lab page
Adds EvaluateUseCase, the C# port of bin/wiki_rank_eval.py: every output/
document with a question and a sources list labels a query, gold is the
cited slugs that still exist as wiki notes, and RankUseCase ranks each
one to report recall@k, hit-rate@k, MRR and a one-hop recall variant that
also credits a gold note reachable from a top-k note by wikilink or
related:. Exposes it as POST /api/v1/lab/eval (write-policy) and a new
/lab page for running it against any registered embedding model and
comparing runs side by side.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 16:39:53 +02:00
claude-botandClaude Sonnet 5 44c48bde71 feat(answer): add Anthropic Messages API answer model adapter
Implements IAnswerModel over Anthropic's streaming Messages API: SSE
parsing (text_delta/usage/error), 429/529 retry with Retry-After or
exponential backoff, and a pure AnswerPrompt.Build for the evidence
and judgements prompt shape. Health check hits GET /v1/models.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 16:37:55 +02:00
claude-botandClaude Fable 5.1 35887a9cbf facts: POST /api/v1/facts deposits a raw source through the store, attributed to the API key
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 16:29:06 +02:00
k.renner 5a6cb8b15a Merge pull request 'Phase 3: typed questions over a retrieved context (POST /api/v1/ask), TypeSafe judge adapter' (#5) from feat/ask into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
Reviewed-on: #5
2026-09-24 13:45:29 +00:00
k.renner 3357ee84be Merge remote-tracking branch 'origin/main' into feat/ask
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
2026-09-24 15:40:39 +02:00
k.renner a02e4df939 Merge pull request 'Phase 2: read API and the MudBlazor pages' (#4) from feat/read-api into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
Reviewed-on: #4
2026-09-24 13:38:51 +00:00
claude-botandClaude Fable 5.1 34cf91ebaf ask: wiki_ask.py as a use case and POST /api/v1/ask; judges from configuration
Rank every query, unite the top notes into one context under a per-document
and a total budget, expand context choices over paragraph or document ids
plus NONE, add the covered gate, judge context choices alone and the rest
batched, attach the answering paragraphs' text. A missing TypeSafe key is a
call-time error shown in status, not a startup failure.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 13:57:17 +02:00
claude-botandClaude Sonnet 5 123850f975 feat(ui): add MudBlazor read pages for search, notes, browse, raw, reports and maintenance
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Adds /  (search, with recent notes as default view), /notes/{slug}, /browse,
/raw/{slug}, /reports/{slug} and /maintenance, all InteractiveServer, built on
NoteQueries and IEmbeddingModel injected directly (no HTTP). Moves the former
status page from / to /status and factors its markup into a shared
StatusPanel used by both /status and /maintenance. Adds a MudDrawer/NavMenu
nav in MainLayout.

Wikilinks ([[slug]], [[slug|alias]], [[slug#heading]]) are rewritten to note
links (or a muted "no such note" span when unresolved) by a new
Wiki.Host.Rendering.WikiMarkdown, before the body goes through Markdig with
DisableHtml() so note content can't inject script. Fenced code blocks and
inline code spans are left untouched. Covered by WikiMarkdownTests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 13:57:14 +02:00
claude-botandClaude Sonnet 5 e10208ee2c feat(ask): TypeSafe System One judge adapter
IJudge over TypeSafe's System One API: serialises noul/choice/score
questions and the caller's state, retries 429/529 with Retry-After or
backoff to 30s, rejects Choice past the 255-option cap before sending,
and skips the request entirely for an empty question set.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 13:53:52 +02:00
claude-botandClaude Fable 5.1 2cd171d3c5 read api: notes with links, backlinks and resolved sources; tags; raw queue; reports
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 13:45:55 +02:00
k.renner d718a83f33 Merge pull request 'store port: get and create documents; depositing a fact is a use case' (#3) from fix/adapter-names into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
Reviewed-on: #3
2026-09-24 11:39:39 +00:00
claude-botandClaude Fable 5.1 6ff8d4bce6 store port: get and create documents; depositing a fact is a use case
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
The fact sink was a second adapter over the same backend. INoteStore
gains CreateAsync (DocumentExistsException on a taken path); the raw
note's shape, slug and collision numbering live in DepositFactUseCase;
GiteaRepository only moves text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 13:37:20 +02:00
claude-botandClaude Fable 5.1 fe6a284c6c gitea: one repository adapter serving both the note-store and the fact-sink port
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
Two classes over the same backend showed as two adapters on the status
page. One GiteaRepository, one HttpClient, one health entry; the host
lists an adapter once even when it serves several ports.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 13:30:17 +02:00
claude-botandClaude Fable 5.1 80c7885dc2 gitea: distinct adapter names for the note store and the fact sink
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Both reported as "gitea" on the status page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 13:26:43 +02:00
k.renner c4e193b08d Merge pull request 'ci: lowercase the image path' (#2) from fix/release-image-lowercase into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline was successful
Reviewed-on: #2
2026-09-24 10:25:05 +00:00
claude-botandClaude Fable 5.1 8b032bcb1f ci: lowercase the image path; the repository name carries a capital
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 12:24:03 +02:00
k.renner b48dd28b9e Merge pull request 'Phase 0+1: solution skeleton, ports, adapters, host with parity to the Python wiki-index' (#1) from feat/skeleton into main
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/release Pipeline failed
Reviewed-on: #1
2026-09-24 10:21:28 +00:00
claude-botandClaude Fable 5.1 ded15eb96b ci: registry login as the bot (org-owned repo); allow manual test runs
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 12:18:26 +02:00
claude-botandClaude Fable 5.1 fba535e03a gitea: retry transient read failures, recycle pooled connections
A long sync over one keep-alive connection to Gitea behind Traefik saw a
TLS connection reset after ~1000 documents and aborted the whole run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 11:49:35 +02:00
claude-botandClaude Fable 5.1 14b858bcfc host: endpoint tests; keep status-code pages off API responses; merge the three adapters
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 11:44:12 +02:00
k.renner 8b02986eff Merge branch 'feat/adapter-gitea' into feat/skeleton 2026-09-24 11:41:33 +02:00
claude-botandClaude Sonnet 5 f67cc7b871 feat(gitea): implement the Gitea note-store and fact-sink adapters
INoteStore reads the vault straight from Gitea (branch HEAD, a paged
recursive tree listing, raw file reads); IFactSink appends a new raw/
file via the contents API, numbering around any path collision. Both
verified against the live gitea.renner.uno API for response shape
rather than the swagger doc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 11:40:57 +02:00
k.renner 4f904f7eb3 Merge branch 'feat/adapter-llamacpp' into feat/skeleton 2026-09-24 11:34:13 +02:00
k.renner 65cf355dc4 Merge branch 'feat/adapter-fileindex' into feat/skeleton 2026-09-24 11:34:02 +02:00
claude-botandClaude Fable 5.1 af58249bc5 host: composition root, health/ready/status, /rank and /webhook, auth handlers, MudBlazor status page, Dockerfile, CI pipelines
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 11:33:56 +02:00
claude-botandClaude Sonnet 5 019f046925 feat(fileindex): file-backed vector index and document catalog adapters
FileVectorIndex persists one binary file per model under index/ with
atomic tmp+rename writes; FileDocumentCatalog persists a single
catalog.json, rebuilding the link graph via WikiLinks.Extract on every
upsert. Both start empty and report health Ok with a reset detail if
their file on disk is corrupt, rather than crashing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 11:32:43 +02:00
claude-botandClaude Sonnet 5 2f6f56c4f0 feat(llamacpp): embeddings adapter over llama-server's /v1/embeddings
Batches requests (default size 2, matching llama-server's limits for long
notes), sorts responses by index, and normalises vectors to unit length.
CheckAsync prefers GET /health, falling back to a one-word embed call when
the server doesn't expose it. Registers as a keyed IEmbeddingModel so
multiple models can be wired up under different names.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 11:31:48 +02:00
claude-botandClaude Fable 5.1 811696da90 application: sync use case (hash diff, per-model embedding, backfill), coordinator, rank, status
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 11:30:45 +02:00
claude-botandClaude Fable 5.1 a26490e5e4 core: domain model, ports, vault parsing; contract suites for note store, vector index, catalog, embeddings
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 11:24:33 +02:00
claude-botandClaude Fable 5.1 30f2dec1f2 chore: solution scaffold (net10.0, ports-and-adapters layout, MudBlazor host, xunit)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-24 11:19:40 +02:00