Repository navigation
Releases
v0.9.0 — the installation reads its own knowledge
Milestone 14 opens. A workspace can read its own knowledge on a schedule and say
what it noticed — and it never writes: a finding that names a write is a
proposal for review, and one that names none is a notice on the run.
Added
- Gardening, per workspace and off until somebody turns it on. A schedule in
hours, a cap on findings per run and another per rule, a record of every run
and a screen that reads it. A workspace is the unit of knowledge and of
review, so it is the unit of tidying; off by default because the first thing
gardening does is make work for a person, and capped because a gardener that
buries the review queue has done harm rather than work. - The gardener files and does not ask (ADR 0042). It runs as the workspace's
system actor, the only actor an installation owns — and a system actor is the
host operator at the command line, whose default policy effect is
allow_direct. A gardener that proposed the way an agent proposes would have
been told it may write and would have tidied the knowledge base without asking
anybody. So filing a finding is a second entry point on the proposal service
where the decision is fixed atrequire_reviewrather than read, and there is
no argument it can be called with that writes: theapplyclosure it hands
over is only ever run by an approval. - Duplicates by content hash. Two active items whose current content hash is
identical — the same words under the same heading twice, because the hash
covers the title and the body and no metadata. The older is kept: the
relations and sources that exist point at it, its history is the longer one,
and the copy is the thing that happened afterwards. - Duplicates by text, which is what that misses. The content hash rests on
the normalisation the file format needs — line endings, trailing spaces, runs
of blank lines — and nothing else, so "Auth strategy" and "Auth Strategy" over
one identical body are two items to it. This folds capitalisation, accents and
where the lines broke as well, and asks of the titles only that they be near
enough by trigram to be one heading. It stops at equality: bodies that are
merely similar are a merge, and a merge needs a provider, because what two
nearly-identical items should become is a draft. - A finding that names no write is a notice (ADR 0043). Four of the rules
this milestone owes name no write anybody could approve, and a review queue
item nobody can approve is one that can never be cleared. So they are recorded
on the run that found them, with the rule and the thing each rests on. Nothing
to approve and nothing to dismiss: a notice is true until the thing it
describes is fixed, and then a later run stops reporting it. - Two notice rules.
orphan_sourceis a source reference no revision cites
at all — a row written and never attached, which in a healthy installation is
none, every run.orphan_attachmentis a file every one of whose knowledge
items has been deleted: bytes kept for knowledge that is gone. Neither has a
button that fixes it, and the ADR says so rather than making the rules wait
for one. - An hourly sweep in the worker role, which asks which workspaces are due
and usually finds none, because the interval is the workspace's setting and
the schedule is not. Registered whether or not any workspace gardens, for the
reason the backup queue is. - A README version the tests hold to the changelog. The image line was
guarded and the sentence above it was not, so the README said v0.7.0 while it
offered the 0.8.0 image.
Changed
- The run record says what each rule did, not whether gardening is on: from
the review queue, a rule that found nothing and a rule that never ran look
identical.findings_filedcounts proposals only, so the run cap — which
exists to keep the review queue readable — never stops a notice rule from
being asked. - What a rule leaves out it leaves out in its own query: an item a reviewer
is already holding, and an item whose finding this gardener had refused.
Filtering those from the result instead starves a capped run, because the
answered items are the oldest and a cap takes the oldest first. A test found
that, not a reading of the code.
Verified
- 1398 tests, and a deliberate break for every claim each new one makes. Two
breaks were initially missed and both were gaps in the tests rather than in
the code; one break found a real defect before any of it, where a whitespace
class inside a template literal reached PostgreSQL as a letter. - On the stand: migrations applying themselves on start, the new queue
registered, the screen refusing anybody withoutworkspace.admin, and a run
that filed a proposal and changed nothing.
Compatibility
Two additive migrations, 0026_gardening and 0027_gardening_notices. They
create the settings and run tables and add one column; nothing existing is
altered and no row is backfilled, because a workspace with no settings row has
the defaults, which is gardening off. Upgrading from v0.8.0 needs the new image
and nothing else, and an installation that never turns gardening on behaves
exactly as it did.
v0.8.0 — the target answers and the sidecar is gone
The last step of Milestone 15, and the screen's missing answer. An operator can
ask the backup target whether a copy would arrive, and the second container that
used to take the copies is gone from the product.
Added
- Asking the target whether a copy would arrive. The storage screen offered
a host, a user, a directory and a key, and no way to find out whether any of
it was right: the first answer came from a scheduled run, which is a
consequence after the click rather than before it (docs/WEB_UI.mdrule 4).
Check the connection makes the connection an upload makes, carrying no
file — oneconnectfor both, because a check built on a second way of
reaching the target would rehearse something else, and the same single
authentication method, verifier, timeouts and pin are the point. - Three stages rather than one verdict, because the server has three and
they are fixed in three different places: the address and the credential are
settings here, the directory is somebody else's machine, and a directory that
exists and refuses a file is that machine's permissions. A stage after a
failure says it was not reached, instead of showing a cross that would send
somebody to fix permissions on a directory nothing opened. - The check writes a byte and removes it. Whether a directory accepts a file
cannot be learned by looking at it, and a check that only looked would pass
where the upload fails. If the removal does not succeed, the check says so and
names what it left behind. - Upgrading an installation that is already running, in
docs/DEPLOYMENT.md§12: the copy to take first and why--keep-days 3650
belongs in that one command, and what to do when an upgrade has to be undone.
Changed
- The sidecar is gone. The
backupcompose service,
infra/backup/backup.sh,infra/backup/loop.sh, and
KNOVERGE_BACKUP_INTERVALandKNOVERGE_BACKUP_KEEPwith them. What took
copies was a shell script that could not reach the write locks, so a write in
flight landed on one side of its archive and not the other; what takes them
now holds every workspace's lock across the dump and the archive, and the two
describe one moment. A complete installation is one application container plus
PostgreSQL again (rule 10). - The check reads what is saved, not what is typed. The secret is sealed and
never returns to the browser, so the button is refused with a reason until the
target has been stored. It is not a run either:last_run_atand the upload
fields are left alone, because a check is not a copy. - Both workflows name
ubuntu-24.04instead ofubuntu-latest, which moves
to a new Ubuntu release on a date GitHub picks — 19 October 2026 for Ubuntu
26. A guard refuses anyruns-onlabel ending in-latest.
Fixed
- Two paragraphs of the deployment guide argued from the sidecar's
existence and were wrong the moment it went: taking a copy on demand was
contrasted with a scheduled service that could not hold the locks, and the
dump order was presented as the thing that makes a copy consistent. They are
one mechanism now, the order is still kept, and the reason it is kept is
written down rather than implied.
Verified
- Against the live installation at
knoverge.njsoft.dev: the schedule took a
copy, it reached the second machine over SFTP, and the sidecar was stopped,
removed, and taken out of the compose file with the application still serving. - The check's stages against a real in-process SFTP server: every stage passing
and nothing left behind, a wrong credential, a missing directory, a directory
that exists and refuses the file, a pin that matches, a pin that does not, and
nothing listening at all.
Compatibility
No migrations: this release adds no columns and reads the ones v0.7.0 shipped.
Upgrading from v0.7.0 needs the new image and nothing else.
An installation still running the sidecar has to stop and remove that container
before pulling this version, because the service mounts two shell scripts this
repository no longer contains. docs/DEPLOYMENT.md §9 keeps the migration as an
upgrade note for exactly that reader. KNOVERGE_BACKUP_INTERVAL and
KNOVERGE_BACKUP_KEEP are no longer read by anything and can be deleted from
.env.
v0.7.0 — backups an operator can see and turn on
Milestone 15 bar its last step: an installation keeps copies of itself because
somebody turned that on in Settings, not because they started a second
container. ADR 0040 decides the shape, and ADR 0041 decides what speaks SSH when
a copy is to reach a second machine.
Added
- A backup is a setting the product owns (ADR 0040). The copy is taken by
the application's own job runner in the worker role, and it is the same
archive the shell script produced: the database first, then the data
directory, a manifest recording where each workspace's ledger stood, every
workspace's write lock held across both, and a staging name moved into place
at the end so that a half-written copy is never a copy. Off until somebody
turns it on, and the screen says so. - A Data and storage screen. Whether to keep copies, how often, how many
days to keep them, what the last run did and when, and the copies on disk with
their sizes. Take one now sits beside the schedule, because an operator who
has just configured this wants to watch one happen rather than wait a day to
find out whether it works. - One SSH target, and the server only ever writes to it. Host, port, user,
directory, and a private key or a password, sealed with
KNOVERGE_ENCRYPTION_KEYand refused outright where there is none, as a
webhook secret already is. Nothing on the far machine is listed or removed by
us, so retention applies to local copies, and the screen says that where the
number is typed. - The server speaks SSH itself (ADR 0041), over SFTP, so the account at the
far end needs no shell and the sealed key never has to become a file for
OpenSSH to read. The host key is pinned the first time the target answers, and
shown as aSHA256:…fingerprint beside thessh-keyscanline that prints the
same thing on the operator's own machine, to be compared by eye. - Forgetting a pinned host key, for a machine that was rebuilt rather than
impersonated. Its own action rather than a side effect of saving, behind a
dialogue that says what it widens and when not to use it; the log keeps the
fingerprint that was trusted, because the column that held it is empty
afterwards and nothing else records it. - An upload that fails does not fail the backup. The local copy is a backup,
the next run tries again, and each copy carries whether it left the machine. KNOVERGE_BACKUP_DIR, and a refusal to start when it is inside
KNOVERGE_DATA_DIRor the other way round. The archive is made of the data
directory, so a copy kept there would be inside the next one: the copies would
double,tarwould read a file that is being written, and the screen would
report success throughout.
Changed
knoverge backupon the command line and the scheduled job are one mechanism
rather than two. Two implementations would have been two formats to restore.knoverge restorenames the clients it is refusing to run beside — the
application name and the state PostgreSQL holds for each — instead of counting
them. A refusal reading1 other client(s) are connectedleft an operator to
guess which one.- The Node line is pinned to the Active LTS line and the configuration now says
so, so the next person to read it does not have to infer the policy from a
version number.
Fixed
- The documentation offered
ghcr.io/gynsus/knoverge:0.3.0in four places
while0.6.0was out — three releases of fixes a reader following
README.mdordocs/DEPLOYMENT.mdwould not have received. - A fresh installation could not take a copy at all. The image creates
/dataand gives it to the user it runs as, and never did the same for
/backups— so Docker created that mount point itself, owned by root, and the
process was refused by it. Pressing Take one now answered
EACCES: permission denied, mkdir '/backups/…', which named neither the user
nor anything to do about it. The image now contains both directories owned by
the user it runs as, which also decides the ownership of an empty volume, and
KNOVERGE_BACKUP_DIRdefaults to/backupsthere for the same reason
KNOVERGE_DATA_DIRdefaults to/data. A directory this cannot fix — one the
sidecar wrote to, which belongs topostgresand is handed back to it on
every start — now gets a sentence naming both uids and the remedy, said before
any workspace lock is taken. - The quickstart did not create every volume the compose file requires.
Copies were given a volume of their own, external like the other two, and
Compose will not invent a volume it is told is external — so
docker compose upended atexternal volume "knoverge-backups" not foundon
a machine where nothing was running yet and nothing to read the cause from.
Both documents now create all three.
Guards
- The external volumes are read out of
docker-compose.ymland each one is
looked for as adocker volume createline in bothREADME.mdand
docs/DEPLOYMENT.md, because the compose file and the instructions for using
it are one list written twice. - Every image pinned in those two documents is compared against the newest entry
in this file, so a release cannot be cut while they name an older one. - Every literal
t('…')key in the web application is resolved against the
English catalogue, plural categories included. A key that does not exist
renders its own name on screen, which no test written from the code notices;
one reached a branch that way.
Verified
Both defects above were found by following the documentation on a machine with
nothing on it and by opening a shell in the container, not by a test. The suite
takes its copies into a directory the test process owns and reads its documents
as text, so it agreed with the code — which is what tests written from the code
do. The quickstart has since been run from an empty project, and a copy taken in
the container against a volume mounted the way the compose file mounts one.
Compatibility
Two migrations, additive. KNOVERGE_BACKUP_DIR has a default, so an
installation upgrades without being reconfigured — but with no volume mounted
there the copies land inside the container and go when it does, which is why the
compose file in this repository mounts one.
The backup compose profile and its two scripts are still here and still work.
An installation running that sidecar cannot share its directory with the
application — the sidecar runs as postgres and takes the directory back on
every start, and the application runs as node — so the move across is: give
the application a backups volume of its own, turn the setting on, watch one copy
arrive, and only then stop the sidecar. docs/DEPLOYMENT.md has it as numbered
steps and says which order leaves an installation taking no copies at all. The
profile is removed in a later release, once nothing is relying on it.
v0.6.0 — a connector lets itself in
Milestone 10: a hosted connector can let itself in, once a person says so. With
this the plan's milestones 0 to 13 are delivered.
Added
- Knoverge is its own OAuth 2.1 authorization server (ADR 0038), because
ChatGPT and Claude.ai cannot be handed a pasted token. A connector given
nothing but the/mcpaddress finds the rest for itself: an unauthenticated
call answers401with aWWW-Authenticateheader naming the protected
resource document, which names the authorization server, which names its
endpoints. Dynamic client registration is open and grants nothing — a
registration holds no workspace, no agent, no permission and no token. - Consent is where a connector becomes an agent. A person signs in, holds
agent.managein the workspace they pick, and says yes; that creates the
agent the connector acts as, at the tier every agent starts at. It then sits
on the Agents screen beside every agent created by hand, and the connection
can be ended there, which revokes every token it issued. Consenting again
resumes the same connection rather than making a second actor, so a person
who reconnects three times is one name in the ledger. - The consent screen names two things and says which is which. The
connector's ownclient_name, chosen by whoever registered it, and the host
the token would actually go to — the one part of the request its author
cannot misrepresent. - An access token is an ordinary agent credential, with a shorter life and
the id of the grant that issued it. Nothing downstream of authentication has
a second case: the same permission checks, the same policy, the same budgets,
the same ledger.
Security
- Authorization codes are spent by the first exchange whether or not that
exchange succeeded, so a stolen code buys no attempts at the verifier. - Refresh tokens rotate, and one presented twice revokes the whole grant: being
able to notice a captured token is not the same as doing something about it. - A redirect URI that does not match a registration is refused without a
redirect, because answering an unmatched URI is the open redirection the
specification warns about. - Client ID Metadata Documents are deliberately not supported: honouring one
means fetching a URL an unauthenticated caller chose, which rule 12 forbids.
Fixed
- Reconnecting a connector made a second one beside it. A hosted client
registers itself again every time it is reconnected, so it arrived under a
new client id and became a new agent — while the connection it had stayed
live, with a working refresh token, because disconnecting in the connector
does not call this server's revocation endpoint. A person who reconnected
twice had three agents and two connections they could not see without going
looking. Consent now recognises a returning connector by the name it calls
itself and the addresses it accepts a code at, keeps the agent it already
was, and retires the connection it is replacing — which the consent screen
names first — and every live connection it holds goes, not the newest of them,
because one left behind is a working refresh token nobody can see. Found by
reconnecting a real connector and reading the tables afterwards. - Disabling an agent did not end its OAuth connections. Its credentials
were revoked and its grant was not, so the connector went on exchanging its
refresh token for access tokens that failed every call — and re-enabling the
agent brought the connection back with nobody having consented again, which a
pasted credential never does. Found by auditing the code against ADR 0038
rather than by a test. - A token issued by consent was not recorded in the ledger. ADR 0038 says a
consent creates an agent and issues a credential, and that both are ordinary
events; the code recorded only the first. Found by connecting a real
connector and reading the feed afterwards. The token a refresh produces stays
out, which is what rule 4 says about authentications. - Signing in dropped the query string of the page you were sent to. Only
the path was kept, and an authorization request is a page whose query is
the request, so somebody not signed in came back to a consent screen with
nothing to consent to.
Verified
Connected from the Claude.ai connector against a live installation over a public
address: discovery, dynamic registration, PKCE, the consent screen, the code
exchange, a tool call, a reconnection and a disconnection. Everything under
"Fixed" above was found that way or by reading ADR 0038 against the code, and
none of it by a test — the tests agreed with the code, which is what tests
written from the code do.
Compatibility
One migration, additive. No new configuration: the authorization server calls
itself whatever KNOVERGE_BASE_URL says, which an installation reachable from
the internet already had to set correctly.
Image
docker pull ghcr.io/gynsus/knoverge:0.6.0
Published for linux/amd64 and linux/arm64; latest points here.
Connecting ChatGPT or Claude.ai
A connector needs a public HTTPS address and will not talk to http://localhost.
Set KNOVERGE_BASE_URL to the address people actually use; everything in the
metadata documents is built from it. Then add the connector with the URL of this
installation's /mcp and nothing else — it finds the rest for itself.
docs/DEPLOYMENT.md section 8 has the detail.
v0.5.0 — knowledge that travels
Milestone 13: knowledge can be taken somewhere else, brought in from somewhere
else, and edited without this product at all.
Added
knoverge exportwrites one workspace as a git bundle, a manifest and, when
asked for, its attachments. Opening it takesgit cloneand nothing from here:
the bundle is the repository, with the Markdown, the taxonomy and every revision
as a commit. The manifest adds what the files cannot say — the workspace's slug,
where the ledger stood, and the display name of each actor the commits name —
and carries nobody's address. What it leaves out it leaves out on purpose:
credentials, permissions, proposals and the event ledger belong to the
installation that made them (ADR 0034).knoverge importputs it back on another installation, with its history.
The commits are walked oldest first, so a workspace that was imported reads like
one that was used: history answers, a diff between any two revisions works, a
summary knows what it was made from. Actors are recreated with their names and
nothing behind them — the interface still says who wrote something, and nobody
gains the ability to sign in or write again. Item ids survive, which makes it a
move rather than a second copy: an installation that still holds those items
refuses the import and names the ids (ADR 0035).- A folder of Markdown arrives as a reconciliation session, not as an insert.
knoverge import-folderopens a session as an agent, submits a compact
inventory — a title, a type and two fingerprints, never the text — and reads
back which notes the workspace already holds, which look familiar and which are
new. Nothing is written until somebody reads that (ADR 0036). knoverge propose-from-sessionis the deciding that follows. Only
new_candidate, each file re-read and checked against the fingerprint the
session recorded, and what happens to each proposal is the agent's policy rather
than the command's.- An Obsidian vault is the same command. A vault is a folder of Markdown with
tags, and tags come across from both places people write them:tags:in the
frontmatter and#tagin the text. knoverge link-from-folderturns the[[wikilinks]]between accepted notes
into relations, so a vault arrives as the graph it was rather than as a thousand
loose notes. Every link becomesrelates_to, because that is all a wiki link
claims.knoverge import-jsonis the path for the exports nobody wrote a parser
for: a Notion export, a wiki dump, somebody's script. Forgiving about what
fields are called and strict about what a record has to be.knoverge adopt-commitsrecords what somebody committed to the repository
by hand. Rule 1 says the knowledge is editable without this product; until now
the answer to a commit made by hand washead_unknownand nothing else. The
file says which item it is, through the id in its frontmatter (ADR 0037).
Fixed
- Recovery lost the provenance of a revision it rebuilt. A write that reached
Git and no further was rebuilt from its commit with its categories, tags, search
index, relations and summary dependencies — and withoutsource_references. So
a recovered item cited a page in its own frontmatter while the database said
nothing rested on it, and "which items came out of this attachment" answered
nothing. Found while starting the import, which rebuilds the same way. - The frontmatter parser required three fields the specification calls
optional.GIT_REPOSITORY.mdnames seven required fields;review,evidence
anddisputedwere also demanded, so a file a person wrote in an editor was
refused. They are defaulted now, and each default is what its absence means.
Guards
- Every command the code defines is asserted to appear in the deployment guide: a
command nobody documented is a command nobody runs.
Compatibility
No migrations, no new configuration. Everything added is a command line
operation; nothing existing changed shape.
Image
docker pull ghcr.io/gynsus/knoverge:0.5.0
Published for linux/amd64 and linux/arm64; latest points here.
v0.4.0 — a workspace can read what its files say
Milestone 12: a picture is described, a recording becomes what was said in it, and the item says which model phrased it. Plus the two things an audit of Milestone 12 called debt.
docker pull ghcr.io/gynsus/knoverge:0.4.0Files become knowledge, whatever they are
- A picture. With a vision model assigned, an image is described and the text on a screenshot or a photographed page is read out. What comes back is an ordinary
documentitem with the attachment it came from as its source, anddrafted_bynaming the model — a description is a model's words about somebody's picture (ADR 0031). - A recording. With a transcription model assigned, an audio or video file becomes what was said in it. The file goes as it arrived and under its own name, because a container this product cannot open is one a transcription server opens every day. Nothing is said about the language: a workspace's own language is a guess about somebody else's recording, and these endpoints detect it.
- A scanned PDF is still not read. A page inside one has to be rendered to an image first, and rendering needs a canvas this product does not have. The file is kept and can be downloaded.
None of this is required. With no model assigned, files behave exactly as they did in v0.3.0.
Connecting the models
Two more purposes on the settings page, each its own model, each connected and stopped on its own. Transcription is offered only from the providers that could answer it — Ollama has no endpoint that takes a recording, and the page says so rather than letting somebody assign a model that can never be asked.
Vision has a probe: a red square on white, a hundred and forty-six bytes, drawn here rather than fetched from anywhere. The screen shows what the model said about it, because a catalogue never says which models can see. Transcription has none, and the wizard says so outright — speech is the one thing that cannot be made up here, and a synthesised clip would test the synthesiser.
An API key is set in the interface now, sealed with KNOVERGE_ENCRYPTION_KEY exactly as a webhook's signing secret is, write-only and never shown again (ADR 0033). KNOVERGE_EMBEDDING_API_KEY still works and is the fallback for the address it names.
A file that was not read can be read again
The sweep only looks at files nobody has read yet, so a model assigned today could never reach a file that arrived last month, and a provider unreachable for a minute left a file failed for good. One button on the Files screen takes one file or every unread one in the workspace.
Five fixes, four of them found by review rather than by anything failing
- A vision model could never be assigned. The constraint on
ai_assignmentsstill listed the two purposes that existed when the table was written, so the call answeredINTERNAL_ERROR. The vision half of Milestone 12 had never worked in the product; it reachedmainand no tagged release. - A claim could expire mid-transcription, so the next sweep could take a file still being transcribed: the same recording paid for twice, and two documents from it.
- The wizard opened a purpose on the wrong model — pressing Change on the vision row pre-filled whatever the embedding model was.
- A replaced provider key was noticed by one process only, so a worker would keep calling with a revoked credential until a restart.
- The wizard would not connect an OpenAI-compatible provider at all, which left transcription reachable only by coincidence.
Two new guards against the class of thing the first of those was: every CHECK constraint listing values is asserted against its contract enum (twenty-three columns), and the README's tool count is checked against the contract.
Compatibility
Two additive migrations, applied on start. No new configuration is required. CHANGELOG.md has the full entry.
v0.3.0 — documents and attachments
Milestone 11: a workspace can hold the documents its knowledge came from.
Added
- Files. Bring a document in through the interface, over HTTP as multipart, or
over MCP as base64. Its text becomes an ordinarydocumentknowledge item —
searched, reviewed, versioned and superseded like anything else — and the file
itself stays under the hash of its contents, beside the repositories and never in
Git (ADR 0008). - An agent's file is not a way past review. The item is written as the person
or agent who uploaded the file, so an agent's document goes to the review queue
exactly as its other writing does, duplicate check included (rule 5). - Text, Markdown, HTML, PDF and Word are read, with no provider and no network
(rule 9). A PDF with no text layer is a scan: kept and downloadable, and reading
one is a later milestone rather than a failure now. - A Files screen that leads with the state — waiting to be read, being read,
read, waiting for review, not readable here, could not be read — explains what
each means, and asks again by itself while the answer is still coming. - Three tools:
attachment_upload,attachment_listandattachment_get,
defined once like every other tool, so MCP andPOST /v1/<tool_name>are the
same operation. The manifest reportsmax_attachment_bytes: what actually fits
through a tool call, base64 included. knoverge integrity checkreportsattachment_missing: a row whose file is not
in the store. An attachment is not in Git and cannot be rebuilt from anything, so
it is the unrepairable kind..github/dependabot.yml, which Milestone 0 asked for and nothing had done:
packages, workflow actions and base images, weekly and grouped.
Changed
- The line that says the job runner started names the queues it actually started,
including webhook delivery and attachment extraction. - The webhook form no longer redraws forty checkboxes per keystroke.
docs/IMPLEMENTATION_PLAN.mdsays where this is: which milestones are delivered
and in which release.SPEC_INDEX.mdis gone. The README's documentation map is the only index, and a
test fails when a document underdocs/is missing from it.
Compatibility
Two additive migrations. Two new configuration variables with defaults
(KNOVERGE_ATTACHMENT_MAX_MB, capped at 128 because an upload is held in memory
while it is hashed). Three new tools and one new field in the manifest's limits;
nothing existing changed shape.
Image: ghcr.io/gynsus/knoverge:0.3.0 (linux/amd64, linux/arm64), also published as latest. Full entry in CHANGELOG.md.
v0.2.0 — webhooks have a screen
Added
- Settings → Webhooks. Adding an endpoint no longer means a request written by
hand inside the container. The screen leads with what leaves — an event and never
the knowledge — then asks for the address, the event types, and whether it is
delivering. The signing secret is shown once, with the check a receiver has to
perform ready to copy. - Each endpoint shows how it is going: consecutive failures, when the next attempt
is due, and what went wrong last. All three were in the API from the first day and
appeared on no screen, so an endpoint that stopped working stopped working
silently. webhooks.listanswerssecret_storage_configured, and an installation without a
KNOVERGE_ENCRYPTION_KEYsays above the button that it cannot keep a signing
secret rather than refusing a filled-in form.next_attempt_aton a webhook summary, so a screen can say when a failing
endpoint will be tried again without a second copy of the backoff rule.
Changed
- Replacing a signing secret asks for the new one, with a button that generates it.
An update that leaves it out keeps the secret it has, so offering to replace it and
then sending nothing would have promised something it did not do.
Compatibility
Additive: two new fields on an existing administrative response. No schema change,
no migration, and nothing about what is delivered or how it is signed has changed.
Image: ghcr.io/gynsus/knoverge:0.2.0 (linux/amd64, linux/arm64), also published as latest. Full entry in CHANGELOG.md.
v0.1.0 — the first release
The first tagged release. Knoverge is a self-hosted knowledge base that humans and
AI agents share: agents propose, people decide, and every material change is a Git
commit and an entry in a keyed hash-chained ledger.
A complete installation is one application container and PostgreSQL. Nothing else
is required, and nothing leaves the installation unless an operator configures it
to.
Canonical knowledge
- Knowledge items are Markdown files with YAML frontmatter in a Git repository, one
repository per workspace, filed under their category path. Agit cloneand a
text editor are enough to read the whole knowledge base without this software. - Every change is a revision and a commit, including a change to metadata alone.
The commit message names the operation, the workspace, the actor, the agent and
the proposal, and carries oneKnoverge-Changetrailer per revision it produced. - Ten item types, one assertion each for the four that hold assertions. Sources and
relations are recorded in PostgreSQL for queries and mirrored into the file as the
portable copy. - Updates carry the revision and content hash they were based on; a stale write is a
REVISION_CONFLICTnaming the current ones, never an overwrite. - Deleting is logical, history is a first-class read — revisions, diff between any
two, restore — and a write that reached Git but not PostgreSQL finishes itself at
the next start. - Validity in time (
valid_from,valid_until,observed_at), so a replacement is
not read as a disagreement; disputes are derived from a livecontradicts
relation and marked at both ends; evidence state is derived from the sources,
corroboratedwhen two of them come from different origins. - Summaries keep the exact revisions they were made from and go stale by arithmetic
rather than by a flag, anddrafted_bynames the model that phrased a revision's
text when one did.
Agents, policy and review
- Agents are identities with bearer credentials: the token is shown once and stored
only as a peppered hash. Trust tiers describe the agent, never grant anything. - Without an explicit
allow_directrule every agent write becomes a proposal,
including for atrustedagent; adenycreates nothing and is recorded. - Four kinds of proposal — create, update, delete, supersede — with review,
approval with a reviewer's edits, rejection with a reason and withdrawal by the
proposer. Nobody decides their own proposal. - Permission grants and policy rules scope by category id with descendants, never by
path; a grant cannot hand out more than the granter holds. - Duplicate detection on the way in: external key, content hash, similar title by
trigram, near meaning by vector. A proposer may acknowledge what they ruled out.
MCP and HTTP
- 26 tools defined once in
packages/contracts; MCP exposes them as tools and HTTP
asPOST /v1/<tool_name>over the same handlers. Administration is deliberately
not a tool. - MCP over Streamable HTTP, with provenance in the call's
_meta— client, model,
session — where HTTP would use headers. - An OpenAPI document generated from the same schemas, describing the refusals as
well as the answers, and aknoverge mcpstdio bridge for hosts that speak no
HTTP. - Per-credential budgets for reads, writes, inventory batches and requests in
flight, plus a backlog budget a rate cannot express: 200 proposals waiting for one
actor.workspace_manifestreports all of them so a client can pace itself.
Reconciliation
- A sync session with a checkpoint, an inventory submitted in batches and six
matching steps — external key, content hash, deterministic metadata, lexical,
semantic, freshness — so a newly connected agent does not re-record what the
workspace already holds. A match outside the caller's read scope is not a match.
Search
- Deterministic chunking, per-language full-text search with
unaccentand
trigrams, optional embeddings, and the two rankings fused by position. The index
is a projection and is rebuilt from the files, never repaired.
Optional model features
- Embeddings, summary drafting and an optional narrative for the activity digest.
Each is configured in the product rather than in the environment, and the core
runs with every provider disabled: search stays lexical and nothing is contacted.
Operations
knovergecommand line: bootstrap, database migrate/status/prune/recover/reindex,
workspaces, taxonomy, agents and credentials,backup,restore,
integrity check,ledger verifyandkeys,audit exportandverify.- The event ledger is append-only in the database itself: triggers refuse
UPDATE,
DELETEandTRUNCATE, and each event is keyed-hashed over the previous hash.
A ledger key is retired rather than replaced, so what it signed still verifies. - Webhooks deliver an event and never an object, signed with a secret sealed with
KNOVERGE_ENCRYPTION_KEY. integrity checkreports ten kinds of disagreement, from an unfinished operation
and a broken hash chain to a missing file, a content or frontmatter hash that does
not match, a taxonomy that disagrees with the tree, and a commit the database has
never heard of.- Docker Compose with the database and application volumes declared external, so
docker compose down -vcannot take a knowledge base with it; optional backup and
Caddy profiles.
The web interface
- Seventeen screens: knowledge, review, taxonomy, sync, agents, policy, workspaces,
the ledger with the activity digest, settings and first-run setup. - Every string goes through the English and Russian catalogues, and CI fails if the
two disagree.
Known limits
- Webhooks are configured through the API or the command line; they have no screen.
- There are no end-to-end browser tests yet: the interface is covered by component
tests and by the HTTP tests behind it. - OAuth for hosted MCP clients, attachments and documents, media understanding,
export/import and knowledge gardening are not started. They are the next
milestones, in that order.
Migration policy
The migrations in this release are the baseline. From here they are immutable:
every change to persistence is a new additive migration with an upgrade test.
Image: ghcr.io/gynsus/knoverge:0.1.0 (linux/amd64, linux/arm64). Full entry in CHANGELOG.md.