Skip to content

kiwiki 4.1.0

Choose a tag to compare

@natorus87 natorus87 released this 29 Sep 16:35
· 4 commits to main since this release
3f34e96

Upgrade notes

  • Refresh tokens are rotated. grant_type=refresh_token now also returns a new
    refresh_token; the redeemed one is consumed (invalid_grant on 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/*, /editor and /settings require 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 carrying Authorization: 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/export additionally requires an authenticated user.
  • Note content could set UI control classes — class on <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>: only wikilink/missing, <code>: only
    language-*), including the HTML export.
  • Session cookie without Secure on direct TLS — the flag depended on KIWIKI_TRUST_PROXY only;
    it now also follows the https request 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 in SECURITY.md.
  • The OAuth registration counter grew without bound — only the requesting IP was pruned. It is now
    pruned globally and capped by KIWIKI_OAUTH_MAX_REGISTER_SOURCES (4096).

Fixed

  • MCP list_all_files violated its outputSchema after a dashboard request — the dashboard enriched
    the cached entries with excerpt; strict clients rejected the whole response for up to 5 s.
    list_all_files now 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
    returned undefined, which ToastUI 3 treats as "render nothing". It now returns context.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, body h1 elements are demoted to h2, so exactly one h1 remains.
  • Invisible selection checkboxes were tab stops — every tree entry cost two tabs. Outside select
    mode they now carry tabindex="-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/file returns an HTMX partial
    without layout. Browser navigation (deep link, shared link, reload — detected by a missing
    HX-Request header plus a text/html Accept) is now redirected with 307 to /?file=, which loads the
    same note in full. Machine clients without text/html still 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, .md appended); missing targets carry the missing
    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/updated now 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 note stays
    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 .kiwiki files were readable through read APIs — validate_content_read_path() existed
    but was only called by read_lines/file_info. read_file, fetch and read_many (and with them
    /api/file, /ui/file and the editor) still served .kiwiki/agent_log.jsonl. The guard now sits
    centrally in read_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 to MAX_QUERY_LENGTH, and the search tool 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 — -32601 was 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 — fetch requires either id (OpenAI contract) or path
    (alias for direct callers) via anyOf instead of nothing; without KIWIKI_BASE_URL the startup logs a
    warning, because citation URLs would be relative and unusable for OpenAI connectors.
  • Dev dependency httpx2 raised 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/ nor tests/ import the package, production was not reachable.
  • Live search never fired — hx-trigger sat on the search form with the changed modifier. htmx
    compares elt.value of the trigger element for that, and a <form> has none: the comparison was
    always undefined === undefined and 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/tags read 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-header is a column flex with
    flex-wrap: wrap there; width is the cross axis and grows to max-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.html loads its <style> after kiwiki-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: block with width: 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.py read the file size limit
    through a dynamic import. It is now a normal import os; the key is visible to the documentation
    check as well.

Performance

  • users.yaml is 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.py fails 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 the title of 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 the kiwiki_sidebar cookie 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+S sits 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.py now checks
    that every KIWIKI_* variable read by the code appears in the README table, that .env.example
    names no invented keys, that values.yaml exposes the UI and session limits and that AGENTS.md
    distinguishes versioned from local paths. .env.example lists the operationally relevant variables
    actively and the rest commented with their default in brackets; the complete list stays in the
    README.
  • AGENTS.md names 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_VERSION is part of the release consistency test — test_release_version_is_consistent
    checked pyproject.toml, Chart.yaml and values.yaml, but not the constant that is shipped as the
    version in FastAPI, /version and the MCP serverInfo.
  • Documented environment variables — KIWIKI_KEY_ATTEMPT_LIMIT (the brute-force counter kept
    separately for the /oauth/authorize form) and KIWIKI_KNOWLEDGE_MIN_FREE_BYTES (the free-space check
    before writing the knowledge index) were missing from the README, .env.example and Helm values.
    Likewise, KIWIKI_MCP_MAX_STAGED_UPLOADS and KIWIKI_MCP_MAX_STAGED_BYTES sat 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 login rate-limit layer
    counted every POST /login permanently, successful ones included. From the sixth attempt on the
    middleware answered 429 before login_submit could 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/authorize is in the oauth tier and uses
    KIWIKI_KEY_ATTEMPT_LIMIT).
  • Search history swallowed its errors — record_search and the prune step at the end of search()
    caught every error with pass. 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.toml required 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 keeps pyproject.toml and ci.yml from 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.