Skip to content

Folio v0.2.0

Choose a tag to compare

@skravtsov skravtsov released this 14 Oct 02:55
· 7 commits to main since this release

A space can now be a public website. Folio AI can run on a key that a space or the whole instance provides, and visitors of a public site can ask Folio AI about it. The rest of the release is smaller, and there is a lot of it.

Publish a space as a public website. Open the space menu, choose "Publish as website" and make the space public. Anyone can then read it without an account: the page tree, search inside the space, documents, live read-only data tables, whiteboards as pictures, forms (view only), PDF and Office files, and downloads as Markdown, PDF or DOCX if you allow them. It is the same space, not a copy, so the site changes when the page changes. Three addresses: https://wiki.example.com/public/<space> works at once, <label>.<PUBLIC_SITES_DOMAIN> needs a domain of its own with wildcard DNS and a wildcard certificate, and a custom domain such as docs.example.com needs a CNAME and a proxy route. Pages with their own access restrictions, the .agent folder and templates are never shown.
How to try it: make a space public, then open /public/<space> in a private window. Operator steps for subdomains and custom domains: Public sites.

A Folio AI key for a space or the whole instance. People no longer need a key each. A space administrator sets a key for one space, an instance administrator sets one for everyone and can switch personal keys off. A run uses the first key that exists: personal, space, instance, then CURSOR_API_KEY. Shared keys are stored encrypted, never shown again and every change is audited. Cursor is still the only provider.
How to try it: space menu, "Folio AI key", or the "Shared key" tab of the Assistant analytics page for administrators.

Ask Folio AI on a public site. A space administrator can switch on "Allow visitors to ask Folio AI about this space". Visitors get an "Ask AI about this space" button and no account is needed. The assistant answers in Ask mode only, from the public pages of that one space, and cannot change anything. It runs on the key of the space or the instance, so the owner of that key pays, and questions are rate limited per address and per site.
How to try it: switch it on in the publishing settings of a public space that has a key, and ask a question on the site.

Not yet: Folio generates no robots.txt or sitemap for public sites, a page restriction is not inherited by child pages (lock each page that must stay closed), and the rate limits are counted per server process.

Also in this release

  • Docs-as-code: front matter from Docusaurus, MkDocs, Hugo or Jekyll is kept byte for byte, a space bound to a folder such as docs/ writes no ids into your files, images can live in the repository next to the page, and moving a page rewrites its links in one commit.
  • OAuth 2.1 for MCP: claude.ai, Claude Desktop and ChatGPT connect to /mcp without a pasted token. New search and fetch tools (23 tools in all), typed output schemas and tool annotations.
  • Connect with GitHub: one click through a GitHub OAuth App, complete repository lists, the real default branch.
  • Replace a PDF or Office page with a new version; every version stays in the history and can be restored.
  • Assistant analytics: ratings, a "did it solve your question?" survey and the questions it could not answer. Space administrators see their own spaces.
  • Status tags, ++underline++, copy that keeps formatting, emoji reactions and "Make heading" on whiteboards.
  • Install kits for ten platforms, a published image for amd64 and arm64, TRUST_PROXY, and a demo mode for people who run their own demo.
  • Security: the assistant has no file or shell tools, raw files follow page access, copy and restore from the trash respect page access, and the open dependency advisories are closed.

Upgrading

Back up first, then update as usual (docs/INSTALL.md). Migrations 031 to 039 apply at startup (037 to 039 belong to the features above). They add tables, columns, indexes and triggers, widen two check constraints and create one system user, "Public visitor", which has no password, cannot sign in and is hidden from lists of people. Nothing is removed. New settings are all optional: PUBLIC_SITES_DOMAIN, PUBLIC_EXPORT_PER_MINUTE, PUBLIC_ASSISTANT_RUNS_PER_HOUR, PUBLIC_ASSISTANT_SPACE_RUNS_PER_HOUR, GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET. A space that is not public behaves exactly as before.

Try it without installing: https://demo.foliowiki.online (the Folio Handbook there is published as a website at https://demo.foliowiki.online/public/folio-handbook, and you can try "Publish as website" on it yourself; the instance is shared and resets every 24 hours). How it works and what to watch before you publish: https://foliowiki.online/blog/publish-wiki-space-as-public-website/. Everything that changed: CHANGELOG.md.

Full changelog for 0.2.0

Folio can now publish a space as a public website, pay for Folio AI with a key
shared by an instance or a space, and let visitors of a public site ask Folio
AI about it. Spaces that live in a product repository stay tidy: front matter
from other tools is kept, ids stay out of the files, images can live in the
repository, and moving a page keeps its links working. Also in this release:
OAuth 2.1 so that claude.ai, Claude Desktop and ChatGPT connect to /mcp,
search and fetch tools, assistant analytics, replacing a file page with a
new version, "Connect with GitHub", install kits for ten platforms and a
public-demo mode.

Upgrading from 0.1.0: back up first (see docs/INSTALL.md). Migrations 031 to
039 are applied automatically at startup. They add tables, columns, indexes and
triggers, widen two check constraints (the source of a saved git credential and
the visibility of a space) and create one system user, "Public visitor", the
identity behind Ask Folio AI on public sites; it has no password, cannot sign
in and is hidden from lists of people. Nothing is removed. Every new setting is
optional, and an unset one keeps its default or leaves its feature off:
PUBLIC_SITES_DOMAIN, PUBLIC_EXPORT_PER_MINUTE,
PUBLIC_ASSISTANT_RUNS_PER_HOUR, PUBLIC_ASSISTANT_SPACE_RUNS_PER_HOUR,
GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, TRUST_PROXY, FOLIO_OG_*,
FOLIO_POSTHOG_KEY and FOLIO_DEMO_*. A space that is not public behaves
exactly as before.

Added

  • Publish a space as a public website. A space or instance administrator
    opens the space menu, chooses "Publish as website" and sets who can read the
    space to "public": anyone on the internet then reads it without an account,
    as a read-only site with the page tree, search inside the space, documents,
    data tables (live, read-only), whiteboards as pictures, forms (view only) and
    PDF and Office files. The site is served at <PUBLIC_URL>/public/<space> as
    soon as the space is public; at <label>.<PUBLIC_SITES_DOMAIN> when the
    operator sets PUBLIC_SITES_DOMAIN (wildcard DNS and a wildcard certificate
    on the reverse proxy) and the administrator picks a label; and at a custom
    domain such as docs.example.com (a CNAME to the instance and a proxy route
    for that host). The administrator can also let visitors download a page as
    Markdown, PDF or DOCX (PUBLIC_EXPORT_PER_MINUTE caps PDF and DOCX per
    address, default 6). Pages with their own access restrictions, the .agent
    folder and templates are never published (a restriction is not inherited by
    child pages, so lock each page that must stay closed), and visitors are not
    sent the names or cursors of the people editing. People who are signed in
    read a public space exactly as they read a space open to the whole instance.
    The publishing settings live in the database, not in Git, and only a browser
    session can change them: not an API token, and not the public demo. The
    access matrix and the spaces list show the new visibility, and switching a
    space to public asks for confirmation. Folio does not generate robots.txt
    or a sitemap for these sites yet. Adds migration 038. See docs/INSTALL.md
    and docs/LIMITATIONS.md.
  • Shared Folio AI key. Folio AI no longer needs a key per person. An
    instance administrator can set one Cursor key for the whole instance (the
    "Shared key" tab of the Assistant analytics page) and a space administrator
    can set one for a single space (space menu, "Folio AI key"); whoever owns the
    key pays. A run uses the first key that exists, in this order: the person's
    own key, the key of the space the question is asked in, the key of the
    instance, then CURSOR_API_KEY from the environment. An instance
    administrator can also turn personal keys off so that everyone uses the
    shared key: a stored personal key is then ignored and the form for adding one
    is hidden. The assistant settings show whose key is in use. Shared keys are
    stored encrypted with FOLIO_SECRET and are never sent back to the browser
    after they are saved (only the key's name, the Cursor account, who set it and
    when); every change is written to the audit log, and the public demo cannot
    change them. Adds migration 037. Cursor remains the only provider.
  • Ask Folio AI on a public site. In the publishing settings a space
    administrator can switch on "Allow visitors to ask Folio AI about this space"
    (off by default). Visitors then get an "Ask AI about this space" button and
    need no account. The assistant runs in Ask mode only, answers from the public
    pages of that one space, cannot change anything, cannot read page history,
    and is told it is talking to an anonymous visitor. It uses the key of the
    space, else the key of the instance, else CURSOR_API_KEY (a visitor has no
    personal key), so the owner of the key pays; with no key the panel says Folio
    AI is not set up for the site. A conversation belongs to the browser that
    started it (a random token whose hash is stored), and nobody else can
    continue or read it. Questions are rate limited per address
    (PUBLIC_ASSISTANT_RUNS_PER_HOUR, default 20 an hour across all public
    sites) and per site (PUBLIC_ASSISTANT_SPACE_RUNS_PER_HOUR, default 200 an
    hour); the counters live in the memory of the server process. Public
    conversations appear in the assistant analytics as "Public visitor". The
    rules in the space's .agent folder still apply to these runs, so keep
    secrets out of them. Adds migration 039.
  • Front matter from other tools is kept. Folio edits only its own keys
    (id, order, status, icon, cover) inside an existing front matter
    block. Keys written by Docusaurus, MkDocs, Hugo or Jekyll (title,
    sidebar_position, slug, tags, description, ...), comments, lists and
    quoting stay byte for byte through a scan, a save, a change of icon or order,
    a rename, a copy and a duplicate. A block that cannot be parsed is left
    exactly as it is.
  • A light footprint in a product repository. A space bound to a folder of a
    repository (a root path such as docs/) no longer gets an id written into
    its files, neither on the first scan nor when a page is created or edited.
    Ids live in Folio's index and survive moves, renames, copies and restores
    from the trash, and the space's .folio metadata file goes into a dotfile
    inside that folder (docs/.<slug>.folio) instead of the repository root,
    where site generators ignore it. Spaces that already carry ids in their
    files, or a <slug>.folio in the root, keep working as they are, with no
    migration.
  • Images in the repository. A space administrator can turn on "Store images
    in the repository" (space menu, "Image storage"). New pasted or dropped
    images are then saved in assets/ of the space's folder (docs/assets/ for
    a space with a root path), committed with the page and linked by a relative
    path (../assets/diagram.png), so they also show on GitHub. Off by default;
    images already in pages do not move.
  • Moving or renaming a page keeps its links working. Relative links and
    images inside the moved page and its subtree are recalculated for the new
    location, and links to it from other pages are rewritten. The move and every
    rewrite are one commit (docs: move <old> -> <new>, docs: rename ... for a
    slug), and Undo restores the old place the same way. Only [text](path)
    links and images are rewritten, not HTML tags or reference-style links.
  • Make heading on a whiteboard. Select a free text block that has a first
    line and a body, in Edit mode, and a "Make heading" button appears beside the
    selection: the first line becomes a larger heading of its own, grouped with
    the body, in one undo step. (Excalidraw has no bold inside a text element, so
    this is how a heading is made.) Text inside a shape or a sticky note is not
    offered it.
  • Connect with GitHub. "Git access" can link a github.com account through
    a GitHub OAuth App instead of a pasted token: one click, GitHub's consent
    screen, and the token is saved for github.com (it replaces a token saved
    there earlier). It appears only when GITHUB_CLIENT_ID and
    GITHUB_CLIENT_SECRET are set, and is also offered in the "create space"
    and "connect git" dialogs. The token form now names the scopes it needs and
    links to the page where the token is created. See docs/INSTALL.md.
  • MCP tools declare their result shape. All 23 tools now publish an
    outputSchema and return structuredContent next to the JSON text they
    always returned, so clients and catalogs that read structured output get
    typed results (lists are wrapped in an object: {"spaces": [...]},
    {"hits": [...]}, ...; the text form is unchanged). Every tool parameter
    now has a description, including limit/offset of folio_table_query
    and the column fields of folio_table_add_column, folio_table_create
    and create_page. Tool names are unchanged. See docs/MCP.md.
  • Replace a file page with a new version. A PDF, Word, Excel or PowerPoint
    page can now take a new file without becoming a new page: it keeps its
    link, its place in the tree, its icon, its access rules and its stars. Use
    "Replace with new version…" in the page's tree menu, the button in the
    page header (editors only), or drag a file from the desktop onto the open
    page and confirm. After a replace a toast offers "Undo". The new file may
    have a different type (a deck that is now a PDF): the file is renamed and
    every link to it from other pages is rewritten, the same way "Change slug"
    does. A file that is byte for byte the current one changes nothing. The
    usual checks apply (.pdf, .docx, .xlsx, .pptx, and the bytes must
    look like the claimed format), and share links cannot replace anything.
  • Version history for file pages, with restore. Every replace is its own
    Git commit, and whatever was still waiting for the quiet-period commit is
    committed first, so the version you replaced is always in the history. The
    history panel of a file page lists the versions; each shows the file name
    and size it had then, a download of exactly that version and, for a PDF,
    a preview. "Restore this version" puts that file back (and its type) as one
    more commit, so nothing is rewritten and the version you restored over stays
    in the history. New endpoints: POST /api/pages/:id/file (replace) and
    GET /api/pages/:id/history/:sha/file (the file at a commit). The MCP
    tool for a page at a revision now answers for a file page with its path,
    extension and size; the bytes are not exposed over MCP. Search still finds
    file pages by title only.
  • Status tags. A short coloured badge inside a sentence, like the status
    lozenge in Confluence: :status[Done]{color=green}. Six colours (grey, blue,
    green, yellow, red, purple); grey is the default and is also what a missing
    or unknown colour falls back to. The text is stored as typed and shown in
    capitals, so search, export and copy keep your own spelling. In Live edit the
    tag is one badge that the caret skips over; click it for a popover with a
    text field and the six colours, and insert one from the toolbar button, the
    selection bar or the / menu (with text selected it wraps the selection).
    Tags also work inside table cells. Outside Folio the source reads as plain
    :status[Done]. Search finds a tag by its text, never by its colour. The
    Confluence import turns status macros into these tags (both the rendered
    lozenge and the storage-format macro), PDF and DOCX export draw them as
    badges, and the assistant knows the syntax.
  • Underline is written ++text++. Underline used to be an HTML tag pair
    (<ins>…</ins>), and a pair like that could cross a ** pair and produce
    text nothing could render. ++text++ is a delimiter pair like ~~ and
    **, so it nests with bold, italic, strike-through and highlight in any
    order and can never cross them. Exactly two plus signs: C++ and a + b
    stay text. The toolbar button and the hotkey write it, paste and the
    Confluence import write it, and the assistant and MCP descriptions tell
    agents to use it. Existing <ins> and <u> on pages still render
    underlined, and the underline button reads them too (pressing it on such a
    span removes it). The conversion is also available as a function for anyone
    who wants to rewrite old pages in bulk: it is conservative, leaves any pair
    whose rewrite would not parse exactly as it was, and counts what it skipped.
  • Copy and cut in Live edit keep inline formatting. Live edit folds the
    markers (**, ==, ++, link brackets) away, so copying the visible word
    used to put bare text on the clipboard. The selection is now mapped back to
    its Markdown source, and the clipboard gets two flavours: Markdown as plain
    text, and the rendered result as HTML, so Slack, Google Docs and mail keep
    the formatting. Pasting that HTML back into Folio (or HTML copied from
    Reading) converts highlights, status tags and strike-through to Folio's own
    Markdown.
  • Live edit lists look like Reading. Bullets are drawn by depth (disc,
    circle, square), nested items indent one level at a time with a hanging
    indent, ordered numbers stay real text, and checklist items drop the dash in
    favour of the checkbox. Source mode is unchanged.
  • Emoji reactions on whiteboard shapes. Select a shape in Edit mode and a
    small reaction button appears at the lower right corner of the selection
    frame; it opens a palette of eleven
    emoji (✅ ❌ 🔧 for yes, no and in progress, then 👍 ❤️ 😂 🎉 🤔 👀 🔥 👏), each
    with a name on hover. A reaction shows up as a chip with a count just below
    the shape; click the chip to add or remove yours, and point at it to see who
    reacted. Reactions are stored in their own shared map of the board's live
    document, one entry per shape, emoji and person, so two people reacting to the
    same shape at the same moment both keep their reaction, and they appear in
    every open tab at once. They do not touch the drawing: no scene edit, no undo
    step, nothing in the board's SVG file or in Git. People who may only view a
    board, and anyone in View mode, see the chips but cannot react.
  • Optional analytics hook for a demo you run (off by default). Folio sends
    no telemetry: with no key set, the browser never loads an analytics library
    and the server never opens a connection to anyone. An operator who runs a
    public demo can set FOLIO_POSTHOG_KEY (and FOLIO_POSTHOG_HOST) to send
    anonymous events to a PostHog project of their own: the kind of page opened
    or edited, the space slug and the steps of connecting an AI app, never page
    text, titles, search words, names or e-mail addresses. Visitors are not
    identified and the visit id lives in the tab's session storage, not in a
    cookie; the browser's Do Not Track setting turns it off. See
    docs/INSTALL.md.
  • Link previews. Links to the instance's front page now carry Open Graph
    and Twitter card tags, so LinkedIn, Slack, Telegram and the like show a title,
    a description and a 1200x630 picture. A plain installation gets neutral
    defaults and a Folio card; the public-demo mode has its own text and card.
    FOLIO_OG_TITLE, FOLIO_OG_DESCRIPTION and FOLIO_OG_IMAGE (a path under
    the site or an absolute address) override any of the three. Share links keep
    showing the shared page's own title and excerpt.
  • search and fetch MCP tools for ChatGPT deep research and company
    knowledge.
    ChatGPT only accepts two read-only tools of exactly these
    names. search(query) returns {results: [{id, title, url}]} and
    fetch(id) returns {id, title, text, url, metadata} (the page as
    Markdown; a data table as a Markdown table, a board as its labels and
    connections, a PDF or Office file as a note). They apply the same access
    rules as search_pages and read_page: a page you may not see is "page not
    found", the same answer as for one that does not exist. The server now has
    23 tools.
  • integrations/: ready-made setups for AI tools. Config files and
    instructions for connecting Folio over MCP: a Claude Code plugin (with a
    folio-wiki skill and a marketplace file, so claude plugin marketplace add evergreen-it-dev/folio works), Cursor (mcp.json and a rule), VS Code,
    Codex, Open WebUI, two importable n8n workflows (a new GitHub issue becomes a
    table row; a weekly summary page), steps for ChatGPT and Claude (OAuth), and a
    description of an onboarding setup. Placeholders only, no tokens.
  • OAuth 2.1 for MCP: connect Folio from claude.ai, Claude Desktop and
    ChatGPT.
    /mcp now answers an unauthenticated request with 401 and a
    WWW-Authenticate header pointing at the protected-resource metadata, and
    Folio is its own authorization server: discovery documents
    (/.well-known/oauth-protected-resource, /.well-known/oauth-authorization-server),
    dynamic client registration and Client ID Metadata Documents, authorization
    code with PKCE (S256 only), a consent screen on top of the normal sign-in
    (read, or read and write), one-hour access tokens with rotating 30-day
    refresh tokens, audience binding to /mcp (RFC 8707) and revocation
    (/oauth/revoke, and "Connected apps" under "API tokens"). Tokens act with
    the rights of the person who approved them; a read connection cannot run
    write tools; everything is stored as hashes. Personal access tokens work
    unchanged. Adds migration 034 (five tables).
  • MCP tool annotations and server instructions. All 21 tools now declare
    title, readOnlyHint, destructiveHint, idempotentHint and
    openWorldHint explicitly, so clients can skip confirmation for reads and
    ask for it on replacing or deleting tools. The server also sends a short
    instructions text on initialize.
  • Install kits and a ready-made image. A workflow publishes the image
    (ghcr.io/evergreen-it-dev/folio, amd64 and arm64), and deploy/ carries
    kits for a plain Linux server (install.sh), Coolify, Railway, Render,
    DigitalOcean, Dokploy, CapRover, Easypanel, Portainer and Fly.io, plus a
    smoke check for a fresh installation (deploy/smoke.mjs). See
    docs/INSTALL.md.
  • Public-demo mode (for running your own demo). Off unless
    FOLIO_DEMO_MODE=1; an ordinary instance never serves any of it. When on,
    the sign-in screen offers one-click demo accounts and a thin banner shows in
    the app. For everyone in that mode the features that could be abused on a
    shared instance are refused with 403: API tokens, the assistant and AI keys,
    git and Confluence endpoints that connect to a host the caller chooses,
    creating spaces, share links, invitations, changing name or username,
    changing a space's visibility or publishing it, and setting shared AI keys;
    uploads are capped (FOLIO_DEMO_MAX_UPLOAD_MB, default 5), and a demo
    account cannot change passwords. See .env.dev.example.
    Demo accounts may be space administrators (to edit a space's .agent
    rules) without being able to break the demo for others: deleting or
    renaming a space, members and roles, git sync, page permissions, emptying
    the trash and removing the .agent folder itself answer 403. /mcp and the
    OAuth credential endpoints are rate limited per address and size capped
    (FOLIO_DEMO_MCP_RPM, FOLIO_DEMO_MCP_WRITES_PER_HOUR).
  • TRUST_PROXY. Behind nginx, Traefik or Caddy every visitor used to
    share the proxy's address, so the sign-in rate limit was one bucket for
    everybody. TRUST_PROXY=true takes the client address from
    X-Forwarded-For, but only from a proxy on a loopback or private address
    and only the entry that proxy appended. Off by default; a bare hop count is
    refused.
  • Assistant feedback and analytics. Every saved assistant answer can be
    rated 👍 or 👎, the panel asks "Did the assistant solve your question?" after
    every third answer, and the assistant records questions it could not answer
    or was unsure about. Instance administrators get an "Assistant analytics"
    page (/admin/assistant) with the conversations, ratings, survey results and
    a "Questions without an answer" list, filtered by space, user and date.
    Opening someone's conversation is recorded in the audit log. Adds migration
    032.
  • Assistant analytics for space administrators. A person who administers a
    space now sees the "Assistant analytics" entry in the account menu, limited
    to their own spaces: only conversations that ran in those spaces, and inside
    them only the messages of those runs. Counts, first question and dates are
    computed over that visible part, a filter by someone else's space is
    refused, and the page says how many messages are hidden. Instance
    administrators still see everything; a personal access token is still
    refused.
  • "Who opened this conversation". The page of a conversation in the
    assistant analytics lists who opened it and when (the latest 50, then "and N
    earlier openings"), read from the audit log. Adds migration 033, a partial
    index only.
  • Paste a link over selected text. Select some words and paste a single
    address: the words become a link to it instead of being replaced. Works in
    the page text and inside table cells. A selection that is already a link or
    an address, a selection inside code, or a clipboard holding anything other
    than one address is replaced as before.
  • Search in the space switcher. Long space lists scroll inside the menu,
    while the action for creating a new space stays visible.
  • Markdown tables have three labelled width modes: narrow, medium and full
    viewport. In narrow and full-width mode the highlighted right edge can be
    dragged to set an exact width. A full-width table may grow past the viewport
    and scroll horizontally inside its own container.
  • A welcome wizard for a fresh installation. Instead of an empty screen
    with one button, someone who has no space yet gets three steps: what to
    create first (a document, a whiteboard or a data table), what Folio can do,
    and what the assistant is. Then the first space is created with the chosen
    page in it, and a first document opens ready to type in. "Skip" goes
    straight to that result. The tour can be reopened from the account menu.
  • Duplicate in the page menu of the sidebar. The copy appears next to
    the original together with all of its child pages, is named
    "Title (copy)" and opens at once. Works for documents, whiteboards, data
    tables, forms and file pages; "Copy to…" remains for a copy in another
    place or another space.

Changed

  • The assistant links the pages behind an unanswered question. The "what
    is missing" text of a question it could not answer now names the gap briefly
    and links the pages the assistant actually read for it, so an administrator
    can jump straight to the page that should have held the answer. Pages it did
    not open are never linked.
  • The space administrator's note shows space names. The line saying which
    conversations a space administrator sees now lists the names of their
    spaces instead of their short addresses.
  • In "Questions without an answer" the person's own question is now the main
    line, with the assistant's restatement below it as "Assistant's wording";
    older reports that were saved without a run show only the restatement. The
    space filter and the space columns of the assistant analytics show space
    names instead of slugs.
  • Interface languages are picked up from the bundle files that exist, so
    adding a language means adding files, not editing code. English is the
    fallback when the browser asks for a language there is no bundle for.
  • Texts the server writes into content — the name of the first view of a new
    table, the heading of the link list on an imported whiteboard — follow the
    language of the person who triggers them.
  • The assistant's instructions are in English; it answers and writes pages in
    the language it is addressed in.
  • Code comments, tests, test data and server error messages are in English.
  • In the share menu the checkbox under an existing link is always labelled
    "Include child pages": the tick alone says whether it is on.
  • Share links can be listed, created, changed and revoked only from the
    browser. A personal access token, whatever its scope, is refused: the link
    holds an access key. Agents still read a page through a link they already
    have.
  • A share link "with child pages" leaves out every page that has its own
    access restrictions, and everything below such a page. The set is checked on
    every visit, so restricting a page later also takes it out of links that
    already exist. A restricted page shared by a link of its own is still shared
    together with its unrestricted children.
  • Raw files (/files/…) follow page access. A page file is served under the
    rules of that page; any other file is served to a signed-in user unless
    every page that references it is hidden from them, and to a share guest
    only when a page of the share references it. A file no page references
    stays open to the members of the space.
  • Uploaded attachments and repository files in an unusual format are
    downloaded instead of being opened in the browser; SVG still opens, but
    without scripts or network access. Common raster images and PDF open as
    before. In particular, a link from a page to a .html, .txt, .md,
    .json or .csv file, or to a video or audio file, now downloads it
    instead of opening it in the tab.
  • URL paths that are not in canonical form (//, /./, backslashes, an
    encoded ..) now answer 403.
  • Running outside Docker needs Node.js 22 (the image already uses it).
  • The assistant checks the space and pageId sent with a request. A space
    the user cannot read answers 404 space not found, exactly as a space that
    does not exist; an unknown or inaccessible page answers 404 page not found; a pageId without space is 400. Earlier an unknown page id was
    silently ignored. An instance admin who is not a member of a space can no
    longer start an assistant run in it.

Fixed

  • Repository lists from GitHub and GitLab are complete, and say why they
    fail.
    The "create space" dialog follows the provider's pagination (up to
    1,000 repositories over 10 pages, never leaving the original host), and a
    provider failure (invalid token, rate limit, SAML single sign-on required,
    forbidden, unreachable) shows a specific message instead of an empty list.
    Creating a space from a repository without naming a branch asks the remote
    for its real default branch instead of assuming one.
  • A link to a page that sorts later is no longer stored as broken. The
    first scan of a space classified each page's links while it walked, so a link
    to a page further down the alphabet was recorded as broken and never counted
    as a backlink. Links are now indexed after the whole walk, and a rescan
    re-indexes unchanged pages that held broken links.
  • Formatting keeps its blanks outside the markers. Every inline formatting
    button leaves a selection made only of blanks alone and keeps leading and
    trailing blanks outside the markers. Pairs are read with the CommonMark
    flanking rules, so an asterisk after a space is not an italic, and pressing
    the button on a broken pair such as *word * moves the markers onto the text
    (*word* ).
  • Headings show plain text in the outline, notes and page title.
    Highlights, status tags, emphasis, links and escapes are resolved, so the
    outline, the notes panel and a page's title (the first # heading) no longer
    show raw markers such as ==text=={.green}. Heading anchors are unchanged,
    so saved links keep working.
  • A new sticky note opens its text field after Escape. On a whiteboard,
    after pressing Escape the next new sticky note used to open the previous
    note's text (or nothing). The new note is now the only selection and opens
    for typing.
  • The built-in assistant no longer gets the MCP tools search and fetch.
    They exist only because ChatGPT deep research accepts tools with exactly
    those names, and they duplicate search_pages and read_page (same access
    checks), so the model saw both reading tools twice, and a bare fetch looks like a
    web fetch to a model that has no way to open a URL. The assistant now sees
    the other 21 MCP tools in Agent mode and the read-only ones in Ask mode. The
    external /mcp endpoint is unchanged and still offers all 23 tools. The
    pageMeta test no longer depends on the environment variables of the
    shell that runs it.
  • docker-compose.yml passes every documented setting to the app.
    TRUST_PROXY, FOLIO_DEMO_MODE and the other FOLIO_DEMO_* settings,
    FOLIO_DEFAULT_REPO_URL and the assistant deadlines
    (CURSOR_AGENT_TIMEOUT_MS, CURSOR_AGENT_TIMEOUT_MS_AGENT,
    CURSOR_AGENT_IDLE_TIMEOUT_MS) were read from .env by nobody, so under
    Compose they silently did nothing. They now reach the container; left empty
    they keep their built-in defaults. .env.example and docs/INSTALL.md
    describe them.
  • Reading: a loose list keeps its number and its text on one line. A list
    with blank lines between items wraps each item in a paragraph, and with the
    marker inside the item the number sat alone on the line above the text.
    Markers now hang in a gutter of their own at every level, which also gives
    wrapped lines a hanging indent. Checklists and lists inside table cells keep
    their layout.
  • Confluence import no longer overwrites the space's home page. Importing
    into an existing space with the default target wrote the imported root page
    over the space's own index.md; the tree never lists that file (the space
    home has its own row), so the imported page vanished from the tree and the
    old home was gone. The same happened for a chosen folder that already had a
    page of its own. The root now takes index.md only where nothing would be
    replaced or hidden, and otherwise goes into its own folder; importing the
    same tree again updates that folder in place.
  • Formatting inside a table cell is no longer erased on the first
    keystroke.
    Cells draw nested formatting (bold inside underline, highlight
    inside bold) instead of showing raw tags and markers, and typing into a
    formatted cell no longer drops its underline and highlight.
  • Someone else's selection no longer hides your text. When a collaborator
    selected a range, everyone else saw the selected lines turn into a blank gap
    after a few seconds of the collaborator being idle. The text itself was
    intact in the editor, the shared document and on the server; only its
    drawing was hidden by the fade that dims idle collaborators. Folio now draws
    remote selections itself, as a coloured mark that never touches the text's
    visibility, and the fade dims only the colour and the caret. Mixed versions
    keep working, since the presence data is unchanged.
  • The reaction button on a whiteboard no longer follows the pointer. The
    button used to pop up at whatever shape the pointer crossed, which got in the
    way of drawing and selecting. It now belongs to the selection: select exactly
    one shape in Edit mode and it appears at the lower right corner of the
    selection frame; it stays hidden while you drag, resize, rotate, type or draw,
    and the palette closes when the selection goes away. In View mode, and for
    people who may only view the board, the chips are still shown but cannot be
    clicked.
  • Typed text can no longer be lost between Live edit and Reading. One
    error raised while the browser saved a local copy of the page used to switch
    the editor's connection to the shared document off for good: from then on
    the editor kept text on screen that the document never received, and it was
    gone when you moved to Reading or reloaded. The connection between the
    editor and the shared text now never switches itself off: after every change
    it checks that both sides agree and repairs any difference, and when the
    editor closes it hands over whatever only it had. The local copy is reopened
    if its storage connection breaks, and it is deleted only after the server has
    confirmed it holds your edits (checked against the server's own state, not
    the socket's "synced" flag). If the server does not take your edits the
    header shows "Not saved", and closing the tab with unconfirmed edits asks
    first. A page you may only view (page access) now opens read-only instead of
    an editor whose typing was silently dropped.
  • The server keeps what a live page holds when a file write fails. A failed
    write of a page file is retried with growing pauses and the room's snapshot
    stays in the database; the next open continues from the newer snapshot instead
    of the older file. Page files are written atomically (a temporary file, then a
    rename), and the temporary files are excluded from Git commits and cleaned up
    if a crash leaves one behind. Carriage returns (Windows line ends) are
    normalized wherever text enters a live page, which also stops two clients from
    rewriting the same span and doubling it. When the server itself replaces all
    of a live page's text (a file changed outside while the page was closed, an
    API or MCP body write, "Take the version from Git"), the replaced text is
    first saved in a backup table that is pruned by age. Adds migration 035.
  • A single - under a paragraph stays a dash. Typing - on the line right
    after a paragraph used to turn the whole paragraph into a heading in the
    editor, because Markdown treats a line of dashes under text as an
    underline-style heading. The editor no longer recognises that form while you
    type; # headings, lists, quotes, bold and rules work as before, and Reading
    mode still renders underline-style headings in existing or imported text.
  • The "Ask" and "Agent" mode labels in the assistant panel and on the first-run
    screen are now translated into Ukrainian instead of staying in English.
  • Asking the same unanswered question twice inside one assistant run no longer
    creates a second report; the text of the first one is kept (or completed, if
    it was empty). The limit of three reports per run is unchanged.
  • The "what is missing" text of an unanswered question in the assistant
    analytics is now rendered as Markdown (links open in a new tab; raw HTML and
    javascript: links are dropped) instead of showing the markup as text.
  • The trash page scrolls. A long list of deleted items no longer runs
    past the bottom of the window with no way to reach the rest.
  • The sidebar page tree now updates by itself when someone else changes it.
    A page created, renamed, moved, deleted or restored by another user, an API or
    MCP client, the assistant, an import or a Git sync used to stay invisible in an
    already open sidebar until the page was reloaded. It now appears within about a
    second. Only people who can read the space are told, and only that something
    changed; the list itself is fetched through the usual access checks, so
    restricted pages stay hidden. During a long import the tree refreshes at a
    relaxed pace instead of after every page.
  • The personal "Undo" history of a deleted space no longer shows up in a new space
    created with the same name.
  • Choosing "New space" in the Confluence import dialog now starts with an empty
    name field instead of the address of the current space.
  • Text typed on the line right under a table no longer joins the table. The
    first character there — a letter or an emoji — used to become one more table
    row and turn the grid back into raw Markdown; a blank line now stays between
    the table and the new text.
  • Emoji from the system emoji panel, dead keys and phone keyboards (anything
    typed through an input method) no longer break tables: in an empty cell the
    text was entered twice, and on the line under a table it turned the grid
    into raw Markdown. Enter, Tab, Escape and the arrow keys pressed while an
    input method is composing stay with it instead of moving between cells.
  • A whiteboard box no longer looks empty in the picture of a board. When a
    board was created or updated by the assistant, or imported, the label of a
    box could be drawn underneath the box in the preview used by a document that
    embeds the board, by the version history and by the .excalidraw.svg file
    in Git, while the canvas itself was fine. The label is now always drawn
    directly above its box, and the stacking order the board was built in is
    kept once the board has been opened and saved. A board that already has the
    problem is corrected by its next save; the server does not rewrite a board's
    file just because the board was opened.
  • The AI assistant starts again in the Docker image. After the Cursor SDK
    update, its native parts (built for glibc) could not be loaded by the
    Alpine-based image, so the first question failed with "Error loading shared
    library" on both x86-64 and ARM (Apple Silicon). The image now carries the
    compatibility layer they need, and the build checks that these parts load,
    so an image in which the assistant cannot start fails the build instead of
    failing for the first user.
  • Opening an invitation while already signed in now offers a primary
    Continue action that accepts the invitation with the current account.
    Signing out remains available as a secondary action.
  • A space typed at the edge of formatted text no longer breaks the
    formatting. With the caret right before a bold, italic, struck-through,
    highlighted or underlined phrase, or before a link, the space now goes in
    front of the hidden marker instead of after it, where it used to turn
    **bold** into plain text with the asterisks showing.
  • The MCP server now reports the product version when a client connects,
    instead of a fixed 1.0.0. The MCP documentation now matches the code: the
    create_page tool takes the text of a new document (REST creation does
    not), and the data table tools are split into three that read and five that
    need the write scope.
  • A change pushed to Git (for example from an IDE) now reaches a page that is
    open in the editor right after the sync. Before, the open page did not know
    about it: the next keystroke or closing the tab wrote the old version over
    the file, and the change was silently lost. This holds for documents, data
    tables and whiteboards. Text typed a moment before the sync is merged with
    the change from Git instead of being dropped.
  • When the same passage was changed in Folio and in Git, the conflict markers
    now show up in the document text and the page stays in the conflict state
    until it is resolved. A data table or whiteboard with a conflict is not
    overwritten until "Take the version from Git" is chosen.
  • "Take the version from Git" on a whiteboard now really restores the
    version from Git: the open canvas switches to it, and closing the tab no
    longer writes the discarded version back to the repository.
  • Two small fixes in the MCP server found while an agent used it. A document
    made with create_page from text that does not start with a heading now
    keeps the title you asked for (it used to be named after the file, for
    example "release-notes"): the heading is added above the text, and a
    leading heading you write yourself is kept and becomes the title. And
    folio_table_insert now stores an unticked checkbox as false in a column
    you did not fill, as adding a row by hand does, instead of null.

Security

  • The request log no longer records the query string of the Google and GitHub
    sign-in callbacks (it carries a one-time code), nor the guest token of a
    public-site Ask Folio AI conversation.
  • The assistant no longer has file or shell tools. The Cursor agent runs
    without an operating-system sandbox, and its built-in read, search and shell
    tools were not confined to the conversation workspace, so they could open
    the Git repositories of spaces the person cannot read. A run now offers the
    model only Folio's own tools (the MCP tool set, the built-in tool that
    records unanswered questions, and read_skill, which returns the shipped
    working instructions by name); access control is therefore the one the
    tools apply to the signed-in person, not the file system's.
  • Asking for a space or a page the caller has no role in at all now gets the
    same "space not found" / "page not found" as for one that does not exist,
    in the assistant and over MCP, so private space slugs and page ids cannot
    be probed. A caller with too low a role (a viewer asking to write) is still
    told so.
  • Page search over MCP and in the assistant returns nothing for a disabled
    user, as every other read already did.
  • Dependency updates close four high-severity advisories. puppeteer-core is
    now 25.x (used only to render PDFs with the system Chromium); its browser
    downloader no longer pulls in extract-zip (symlink path traversal, no fixed
    version) or basic-ftp. The nanoid copy bundled through the Mermaid import
    of whiteboards is raised to 5.1.11 or later. npm audit reports no known
    vulnerabilities. Running outside Docker now needs Node.js 22.12 or newer.
  • A second round of dependency updates closes the seven advisories that were
    still open. @modelcontextprotocol/sdk is 1.31 or later (the OAuth client now
    binds stored credentials to the authorization server that issued them; Folio
    itself only uses the SDK's server side and in-process client, so nothing
    changes for it). @fastify/busboy, behind multipart uploads, is 3.2.2 (CRLF
    injection through the filename in Content-Disposition), source-map-js is
    1.2.2, and the Express copy of proxy-addr that the SDK brings in is 2.0.8.
    The development-only shell-quote (via concurrently) is raised with an
    override, and so is katex (via Mermaid, 0.18.2 or later). sprintf-js has
    no fixed version; it was only reachable through the command-line tool of
    js-yaml 3 (inside gray-matter), which Folio never runs, so an override now
    gives that tool a dependency-free argparse and the package is gone from the
    lock file. npm audit --omit=dev reports no known vulnerabilities. A reminder
    for operators: TRUST_PROXY takes address lists as written, so write IPv4
    proxies in plain form (10.0.0.0/8), not as a short IPv4-mapped IPv6 prefix.
  • "Copy to…" and "Duplicate" no longer carry over what the person copying
    cannot open. Before, an editor (or a viewer who could edit another space)
    could copy a page together with its children and read, in the copy, a child
    hidden from them by page access, with its files; copying the root page of a
    space into another space also took the .agent folder along. Now a page
    hidden from the person copying is left out together with everything below it
    and the files only such pages use, the .agent folder is not copied unless
    the person administers the space, and dot files and folders are never
    copied. The copy of a page that has its own access rules is private to the
    person who made it; sharing it again is a deliberate step.
  • A page restricted by page access that was deleted and then restored from the
    trash used to come back open to everyone in the space, and so did restricted
    pages inside a restored folder or a restored space. The trash now keeps who
    could open each restricted page and puts that back together with the page, in
    the same step that makes it visible, narrowed to people who are still members
    of the space. An administrator who was never let into a restricted page no
    longer sees its title or path in the trash. Items that were already in the
    trash before the fix carry no record of their access and are restored as
    before.
  • Raw files of a space can no longer be read around page access, and a share
    link to one page no longer opens other files of its space (other pages'
    Markdown and tables, repository internals). Hidden paths (anything starting
    with a dot, such as .git), metadata files and symbolic links are never
    served.
  • A share link "with child pages" no longer publishes pages that the person
    who made the link cannot read themselves.
  • Share-link management is closed to API tokens, so a read-only token can no
    longer obtain the address of an edit link.
  • Uploaded attachments are served by their detected content type rather than
    the type the uploader claimed. Verified raster images and PDF are shown
    inline, SVG inline under a policy that blocks scripts and network access,
    other known file types as downloads, anything else as an opaque download;
    every response carries X-Content-Type-Options: nosniff. Responses a
    browser has already cached keep their earlier headers until the cache
    expires.
  • Files served straight from a space's git repository (/files/…) get the
    same safe headers as uploaded attachments: the type comes from the content,
    SVG opens under a policy that blocks scripts and network access, and HTML,
    XML, scripts and unknown types are downloads. Anyone who could commit a file
    to a space used to be able to run a script in a visitor's session this way.
  • The assistant no longer loads the .agent rules of a space the user cannot
    read. Rules restricted by page access are skipped for people they are hidden
    from. Rules of a space remain part of every member's run, as documented.
    Turns an earlier conversation already contains are not rewritten.
  • PDF and DOCX export no longer lets page content or an SVG make the server's
    rendering browser request external or internal addresses. The page is
    rendered with scripts off, every request except inline data blocked, a
    content policy inside the document and a dead network proxy. Remote images
    in a PDF remain possible only where the operator explicitly allows them;
    that setting never applies to SVG rendering for DOCX.
  • Updated dependencies with known advisories: Fastify, the Cursor SDK (which
    also drops an old undici), undici, fast-uri, hono, qs,
    ip-address, brace-expansion and dompurify.
  • Upgraded @fastify/static to 10.x, which closes a route-guard bypass through
    encoded separators, a path traversal and a bypass through non-canonical URL
    paths.