Repository navigation
kiwiki 4.1.0
Upgrade notes
- Refresh tokens are rotated.
grant_type=refresh_tokennow also returns a new
refresh_token; the redeemed one is consumed (invalid_granton reuse). Spec-compliant OAuth
clients (ChatGPT, Claude) pick up the new token automatically. - Bearer headers no longer open UI routes. Only
POST /(JSON-RPC) accepts a bearer token
without a session;/ui/*,/editorand/settingsrequire the browser session. REST (/api/*)
and MCP (/mcp) are unchanged. - Raw API-key cookies are deprecated. They keep working but log a warning; please switch to
Authorization: Bearer.
Security
- A bearer header bypassed the session check on every UI route — the WebAuthMiddleware let any
request carryingAuthorization: Bearer <anything>through to/editor,/ui/*and/settings
unchecked; data only stayed private because the namespace was missing. The bypass now applies to
POST /(JSON-RPC connectors) only, and/ui/exportadditionally requires an authenticated user. - Note content could set UI control classes —
classon<a>was allowed freely (kw-file-link,
btn btn-danger…): app-styled buttons inside notes and global click handlers firing without data.
nh3 now filters classes through an allowlist (<a>: onlywikilink/missing,<code>: only
language-*), including the HTML export. - Session cookie without
Secureon direct TLS — the flag depended onKIWIKI_TRUST_PROXYonly;
it now also follows thehttpsrequest scheme. - Legacy cookie auth with a raw API key logs a deprecation warning (once per user and process);
the limits of the in-memory refresh-replay protection are documented inSECURITY.md. - The OAuth registration counter grew without bound — only the requesting IP was pruned. It is now
pruned globally and capped byKIWIKI_OAUTH_MAX_REGISTER_SOURCES(4096).
Fixed
- MCP
list_all_filesviolated its outputSchema after a dashboard request — the dashboard enriched
the cached entries withexcerpt; strict clients rejected the whole response for up to 5 s.
list_all_filesnow returns copies. - Wikilinks in
~~~blocks and inline code turned into visible, escaped<a …>text; both now stay
literal. The duplicate-title check ignores leading horizontal rules and headings inside code blocks. - The editor preview did not resolve wikilinks relative to the source note — it now matches the
server (verified by a test against_resolve_wikilink). - The editor preview was empty apart from wikilinks and code — the custom text-node renderer
returnedundefined, which ToastUI 3 treats as "render nothing". It now returnscontext.origin();
the browser smoke test checks heading, paragraph, table cell and task text in the preview. - Note view without a visible main heading — the server (
title_redundant) and CSS
(h1:first-child { display: none }) together hid both titles. The CSS rule is gone; when the title
differs, bodyh1elements are demoted toh2, so exactly oneh1remains. - Invisible selection checkboxes were tab stops — every tree entry cost two tabs. Outside select
mode they now carrytabindex="-1". - Escape collapsed the desktop sidebar instead of closing open search results. Search results take
precedence; Escape now only closes the mobile overlay. - Excerpts showed raw wikilink syntax (
[[../adr-001-sqliteADR 001]]) and repeated the title.
Wikilinks become their label or file stem, and a leading heading equal to the title is dropped — in
the dashboard and in search results. - Task lists (
- [ ]/- [x]) render as disabled checkboxes in the note view, matching the editor
preview. The checkbox is inserted after nh3; note content still cannot inject an<input>. - Knowledge page with the engine turned off showed "0 nodes / 100 % depth" and a rebuild button that
could only fail. It now explains how to turn the engine on. - Directly opened note URLs showed an unstyled fragment page —
/ui/filereturns an HTMX partial
without layout. Browser navigation (deep link, shared link, reload — detected by a missing
HX-Requestheader plus atext/htmlAccept) is now redirected with 307 to/?file=, which loads the
same note in full. Machine clients withouttext/htmlstill get the fragment, so citation URLs remain
fetchable. [[Wikilinks]]are clickable — they used to be dead text in the note view and the editor preview.
Server-side (_render_markdown_safe) and in the ToastUI preview (customHTMLRenderer) they resolve
by Obsidian convention (relative to the folder,.mdappended); missing targets carry themissing
class and a dashed underline, code blocks and inline code stay literal.- Frontmatter no longer leaks into the editor preview — the
---block, title and tags rendered as a
horizontal rule, heading and body text. Text nodes inside the frontmatter range are suppressed and empty
shells (rule, empty leading blocks) are removed by an observer; the source is untouched. - "Recently created" reported "no note" although notes existed — the filter required a frontmatter
created. The fallback is now the filesystem time (birthtime, else ctime, else mtime);updated
without a stamp falls back to mtime. - Every timestamp read "Today, 00:00" — writes stored only the date.
created/updatednow include
the time (UTC, minute precision); the display still converts to local time. - Search fields name their scope — the top bar (
Suchen…) and the sidebar (Filtern…) were two
magnifying glasses without a promise. Placeholders now carry verb and target ("Search notes…" /
"Filter file tree…", German equivalents). - Dashboard lists show a preview and tags — previously only title and date, i.e. clicking blind.
Every row now has an excerpt (first ~120 characters without frontmatter/Markdown syntax, files over
100 KB excluded) and up to 3 tags. - The mobile CTA row fits at 375 px — three buttons edge to edge are gone: only
+ New notestays
full width, Tags/History move into a···menu (native<details>, closes on selection). Section
labels use tighter tracking on mobile (0.18em → 0.06em). - The start page uses its empty half — a new tag cloud (
/ui/tags?compact=1, top 12 chips, no
breadcrumb or lists); without tags the section stays hidden. - Duplicate H1 in the note view — when the frontmatter title equals the first body H1, the view
heading is hidden (breadcrumb and metadata remain); different titles are shown as before. - Internal
.kiwikifiles were readable through read APIs —validate_content_read_path()existed
but was only called byread_lines/file_info.read_file,fetchandread_many(and with them
/api/file,/ui/fileand the editor) still served.kiwiki/agent_log.jsonl. The guard now sits
centrally inread_file()and_read_frontmatter_only(); writing was already blocked and stays so. - MCP and UI search had no query limit — only REST capped at 512 characters via
SearchRequest.
search()now truncates server-side toMAX_QUERY_LENGTH, and thesearchtool schema declares
maxLength: 512. An unbounded megabyte query ran through FTS sanitising and the LIKE fallback and
burned CPU. - OAuth refresh tokens rotate — previously a refresh token stayed reusable for 30 days. Every
successful redemption returns a new refresh token and consumes the presented one (reuse →
invalid_grant, in memory like codes and DCR clients). Failed client/resource binding checks
deliberately do not consume the token. - Open DCR registration is limited per IP — 128 slots with a 24 h TTL could be filled from a single
source, after which legitimate clients got 503. Now at most 16 registrations per source IP and hour
(KIWIKI_OAUTH_MAX_REGISTER_PER_IP); only successful registrations count. - Unknown MCP methods return HTTP 200 —
-32601was delivered as HTTP 404 and made the server look
broken. Method-not-found is a protocol error, not a transport error, and belongs in the JSON-RPC error
body with HTTP 200. - Stricter MCP schemas for OpenAI clients —
fetchrequires eitherid(OpenAI contract) orpath
(alias for direct callers) viaanyOfinstead of nothing; withoutKIWIKI_BASE_URLthe startup logs a
warning, because citation URLs would be relative and unusable for OpenAI connectors. - Dev dependency
httpx2raised to 2.13.1 — 6 known CVEs in 2.7.0 (WSS over SOCKS without TLS, SSE
ReDoS, multipart header injection, decompression bomb, request smuggling). Dev scope only; neither
app/nortests/import the package, production was not reachable. - Live search never fired —
hx-triggersat on the search form with thechangedmodifier. htmx
compareselt.valueof the trigger element for that, and a<form>has none: the comparison was
alwaysundefined === undefinedand discarded every input and submit event. Typing showed nothing,
Enter showed nothing, and a previously visible result list stayed up after the query changed — i.e.
results for a different query. - The tag overview only saw the root folder —
/ui/tagsread the top level only via
list_files(".")and reported "No tags yet" as soon as notes lived in folders. It now uses
list_all_files("."), which is recursive and already returns the tags. - The file header overflowed the screen on narrow devices —
.file-headeris a column flex with
flex-wrap: wrapthere; width is the cross axis and grows tomax-content. A long path pushed the
header, including the copy button, out of view. - Dashboard panels clipped the timestamp — grid children have
min-width: auto; a long note title
stretched the panel past the edge. - The hero stayed at full display size —
index.htmlloads its<style>afterkiwiki-polish.css,
so the hero rules there lost at equal specificity: the title stayed at 3.8rem and the grid kept the
empty second column of the removed status panel. - Markdown tables had an oversized border —
display: blockwithwidth: 100%drew the border
around the full column width while the cells only filled their content. __import__("os")in the indexer hot path —app/knowledge/indexer.pyread the file size limit
through a dynamic import. It is now a normalimport os; the key is visible to the documentation
check as well.
Performance
users.yamlis only re-parsed when it changes (stat signature) instead of several times per request.
Changed
- Start page for populated workspaces — the introduction (large logo, explanatory text) is reserved
for empty workspaces; otherwise a compact "Pick up where you left off" header with the actions.
"Recently edited" and "Recently created" are one list with a toggle instead of two largely identical
columns; the tag cloud sits next to it in the first viewport. - Tags view as a dense list — one tag per row with its count and the notes visible directly,
instead of a card grid with a "1 file" disclosure. - Heading structure — Tags, Search history and the editor have an
h1(visually hidden in the
editor, where the path field provides the context). - Settings show the file tree instead of an empty sidebar column.
- Mobile note actions — Export and Delete live in the
···menu next to Edit instead of Delete
wrapping alone onto a second line. - The login hint addresses users instead of operators (no more
user:key:role). - Copy icon next to the path with a pointer target of at least 24 px.
- One measurement system instead of two competing ones — the stylesheets carried 37 font sizes and
58 spacing values, 71 % of the spacings fell between steps. Both now run on tokens: seven type roles
plus display, spacings as multiples of 4 (plus 2 px for hairline gaps). Font weights are limited to
400/500/600/700 — 620 and 650 were artefacts of a variable font.tests/test_design_tokens.pyfails on
raw values from now on. - The start page helps on first launch instead of collapsing the help — a fresh workspace showed two
empty panels with the same sentence and a collapsed "Getting started". The help block stays open as
long as no note exists, and each panel has its own empty text. - Timestamps are readable — instead of
2026-09-19T17:42:00, lists and the file header show "Today,
09:31", "Yesterday, 18:25", "3 days ago" or "19 Sep"; the full timestamp stays in thetitleof the
<time>element. - The file tree starts open and remembers its state — it is the main navigation and was collapsed
again after every page change. Open/closed now lives in thekiwiki_sidebarcookie so the server
renders it correctly right away. - The start page shows the work first — the hero is more compact, the status panel with its three
unchanging facts (.md,FTS5, role) is gone, and "Getting started" including the MCP access details
is complete but collapsed below the note lists. - Search results lead with the title — below it the path, below that an excerpt around the match
instead of the first 200 raw characters of the file. Frontmatter, heading markers, table pipes and link
targets are stripped. - The reading view names the path once instead of three times — breadcrumb, title and path chip said
the same thing. The breadcrumb now ends at the folder; the metadata line carries path, time and author. - Edit is the primary action — in the reading view and the editor, Delete was the most prominent
button (on phones even the widest). It keeps its label, sits apart and only turns red on hover/focus. - Tag overview and search history are styled — both views had no CSS of their own and no visible way
back; both now share the reading view's header with a breadcrumb. - Settings no longer waste an empty column — the sidebar there only carried a Back button that
already existed in the page header. Ctrl+Ssits on the Save button — previously a loose label at the end of the editor bar.- The configuration docs stay in sync with the code —
tests/test_deployment_config.pynow checks
that everyKIWIKI_*variable read by the code appears in the README table, that.env.example
names no invented keys, thatvalues.yamlexposes the UI and session limits and thatAGENTS.md
distinguishes versioned from local paths..env.examplelists the operationally relevant variables
actively and the rest commented with their default in brackets; the complete list stays in the
README. AGENTS.mdnames the paths that exist — the file pointed to.Codex/rules/,.Codex/skills/and
.Codex/agents/. The real location is.claude/. It now also notes that.claude/is excluded by
.gitignore, so a fresh clone has no rules, agents or skills, and lists the quality gates that must
be green before anything is reported as done.APP_VERSIONis part of the release consistency test —test_release_version_is_consistent
checkedpyproject.toml,Chart.yamlandvalues.yaml, but not the constant that is shipped as the
version in FastAPI,/versionand the MCPserverInfo.- Documented environment variables —
KIWIKI_KEY_ATTEMPT_LIMIT(the brute-force counter kept
separately for the/oauth/authorizeform) andKIWIKI_KNOWLEDGE_MIN_FREE_BYTES(the free-space check
before writing the knowledge index) were missing from the README,.env.exampleand Helm values.
Likewise,KIWIKI_MCP_MAX_STAGED_UPLOADSandKIWIKI_MCP_MAX_STAGED_BYTESsat after the knowledge
paragraph instead of in the configuration table, where no tool picked them up. - After five failed attempts the correct API key was locked out — the
loginrate-limit layer
counted everyPOST /loginpermanently, successful ones included. From the sixth attempt on the
middleware answered 429 beforelogin_submitcould run and release the budget: anyone who had
mistyped twice could not get in for 60 seconds, even with the right key. The login path now runs
through and decides by the response — only a redirect after a successful check releases the window,
a 401 does not. The OAuth form behaves as before (/oauth/authorizeis in theoauthtier and uses
KIWIKI_KEY_ATTEMPT_LIMIT). - Search history swallowed its errors —
record_searchand the prune step at the end ofsearch()
caught every error withpass. A user saw an empty history and could not tell whether search was
broken or had found nothing. Both paths now log; the same applies to closing the connection pools and
checking the search index schema. - Two dashboard panels reported errors as "empty" — the fragments for "recently edited" and "search
history" returned an empty response on any error. On the dashboard, empty means "nothing there", so
an error was indistinguishable from an empty workspace. - The browser smoke test was a silent gate — it printed nothing on success, so CI showed an empty
line and could not tell "all green" from "nothing ran". - The coverage threshold was 19 points below reality —
pyproject.tomlrequired 60 %, actual
coverage was 79 %. A gate that easy to meet does not protect anything. The threshold is now 75 % in
both files, and a test keepspyproject.tomlandci.ymlfrom drifting apart.
Docker: natorus87/kiwiki:4.1.0 (also :v4.1.0, :latest) — linux/amd64 + linux/arm64, SBOM and provenance, built in CI. Digest sha256:201591a7655e86bfd155ef0de1df97a199af3cd44390285ec78fdc16300a66e6.