Changelog¶
All notable changes to the pgokf PostgreSQL extension are documented in this
file.
The format is based on Keep a Changelog, and this project aims to adhere to Semantic Versioning. The public API contract and what "a breaking change" means for this extension are defined in docs/api-stability.md.
Unreleased¶
0.2.0 - 2026-09-09¶
Skill packages are catalog content, and a web UI to work with them. A
bundle may now carry Agent Skills packages whose
exact bytes the catalog stores and serves back; pgokf-web puts a UI and a
JSON API over the whole catalog, with an optional human workflow (upload,
edit, review) once an identity mode and a writer connection are configured;
pgokf-workspace turns a catalog selection into an agent-harness tree; and
pgokf-mcp speaks HTTP as well as stdio, with bearer tokens and roles.
ALTER EXTENSION pgokf UPDATE TO '0.2.0' is additive: it creates the new
tables empty. A bundle that already carried a SKILL.md projects into
pgokf.skills / pgokf.scripts / pgokf.reference_documents on its next
refresh_bundle (or re-registration) - including a resource-less manifest
whose bytes did not change, which the refresh now re-projects rather than
leaving a plain document.
Added¶
- Skill packages are catalog content, and plugins are built from them
byte for byte (specification §5.3, §15-§18, §21; the schema step to
0.2.0). A bundle may carry Agent Skills
packages: a directory with a
SKILL.mdand optionalscripts/,references/, andassets/. Discovery (okf-sync) now classifies every file (FileClass: document, skill manifest, script, reference, asset, reserved) with the specification's precedence and the nearest-manifest ownership rule, and reads package resources whatever their extension.okf-parsergainsparse_skill_manifest, which projects the portable Agent Skills frontmatter onto a virtualtype: Skillconcept (title=name, thetagsextension, the complete frontmatter undermetadata.agent_skill) and resolves the manifest's links to its own resources; structural findings against the standard are warnings. The extension stages scripts (UTF-8 required) and references/assets as virtualScript/Referenceconcepts whose ids are their full paths, keeps their exact bytes in three new tables (pgokf.skills,pgokf.scripts,pgokf.reference_documents) regardless ofstore_source, re-projects a package whenever any member changes (the §9 package hash), projects membership edges (link_kind = 'package',USES/REFERENCES) and relabels the manifest's own links, and re-identifies a file whose class changes when aSKILL.mdappears or disappears beside it. Three reader-level, tenant-scoped, audited readers return the bytes:get_skill,get_script,get_reference(composite typesskill_result,script_result,reference_result; the access log records the three operations).register_bundle_contentaccepts a package in memory and refuses a loose non-Markdown path. Links resolve by target path as well as by id, so a document's link to a package's.mdreference is an edge and survives the file becoming (or ceasing to be) a resource. Upgrade scriptpgokf--0.1.16--0.2.0.sql. Deferred to a later release: the.okf-package.yamlsidecar, standalonetype: Scriptand typedReferencedocuments, the diagnostics table,relationshipsfrontmatter, and visibility enforcement (the columns are projected and propagated but consulted nowhere - not in search, and not in retrieval, sovisibilityis descriptive in 0.2.0 and is not an access control). -
The plugin builder (
pgokf-workspace, the web Plugins page, the MCPbuild_workspace_plugin) copies a selected skill package whole through the audited readers:SKILL.mdunchanged, scripts executable, references and assets byte-identical, beside the knowledge skill for a native Agent Skills consumer (.claude/skills/<name>/) or underknowledge/skills/<name>/for the other shapes, with the package hash in the lockfile and a Skills section in every index; a resource selected on its own is written at its catalog path. The concept page gains a Package tab (resources with sizes and SHA-256s, exact-byte downloads through/resource/...), scripts and plain-text references render verbatim, binary assets say so, and the JSON API reportspackage/resource. New MCP toolget_skill. -
pgokf-web, a web UI and JSON API companion. Search (lexical on the configured backend, plus semantic and hybrid when an embeddings endpoint is configured) with facets and keyset paging, bundle browsing, concept pages with rendered Markdown, provenance, metadata, links with an interactive 3D link graph (server-rendered SVG without WebGL), similar concepts and history, and an operations page overhealth(),search_index_status(),get_config(), stale and duplicate concepts, and the sync log. Filters without query text browse the matching concepts, so tag, type, status, and bundle links list directly. Read-only throughpgokf_reader(page views read the projection tables; only the source download goes through the auditedget_concept_source()); catalog content is sanitized before rendering and every response carries a Content-Security-Policy; statement, request, and pool-wait timeouts bound every request. Ships in the companions image and as the compose stack'suiprofile (PGOKF_UI_BIND,PGOKF_UI_PORT,PGOKF_UI_TITLE,OKF_UI_TENANT). pgokf-workspace, the workspace injector of spec §21 as a library, and the two fronts over it: the web UI's Plugins page and the MCP server'sbuild_workspace_plugin/list_plugin_targetstools. A catalog selection (bundle, query, types, tags, ids, trust, limit) becomes an Agent Skills package (SKILL.mdplus one reference file per concept) for Claude Code, Codex, Hermes Agent, Kimi, Gemini CLI, Cursor, or the shared.agents/skills/directory, anAGENTS.mdinstruction file, an Ollama prompt bundle (Modelfile and system prompt), or a generic tree, always withokf-workspace.yamlandokf-workspace.lock. Optional components make it more than a skills directory: the harness's MCP server entry forpgokf-mcp(connection string referenced, never written), a catalog guide, and anokf.shhelper over the JSON API. Every target and MCP-configuration layout was read from the harness's documentation and is recorded with its source; unknown targets are refused. Builds are byte-identical for an unchanged catalog.- Graph explorer (
/graph): the catalog-wide link graph (best-connected concepts of the catalog or one bundle, coloured by bundle or type) or a concept's neighborhood, with clickable nodes and edges (hovering or selecting an edge highlights it and its ends; edge cards show the link texts; a node's card lists its connections for touch), zoom, center, 2D/3D, and full-screen controls, a finder, and "explore from here"./api/graphserves the same document. - Portable Agent Plugins. The builder's first target,
agent-plugin, writes a self-contained directory as the Agent Plugins Specification 1.0.0 defines it:plugin.json(the closed portable manifest, versioned by the catalog's newest sync plus a content digest),skills/holding the knowledge skill and every stored package byte for byte, andmcp.jsonwith a typed stdio entry that startspgokf-mcp --env-file ${PLUGIN_DATA}/pgokf.env, so no connection string and no ambient variable is needed.pgokf-mcpgained--env-file; a flag wins over the file and the file over the environment, so an installed plugin always talks to the catalog its own file names. - Picking specific files. A selection may name files by identity
(
picks,bundle_id:concept_id) on top of the narrowing selectors; picks are ordered first so the limit never drops them, and a pickedSKILL.mdbrings its whole package while a picked script or reference is one file. The builder page browses a bundle's files in a tree (directories, skill packages and their members marked) and ticks them into the selection; the MCP tool takespicks;/api/bundles/{id}/treelists a bundle for it. - The Agent Plugin builder page is a five-step flow: say what you are building (an Agent Plugin, a skills package, an instruction file, a prompt bundle, or generic files) and for which agent (a searchable list per kind; a name that is not listed is added as a new agent, with its skills directory for a skills package), name it, choose the content (browse a bundle or all bundles, find files, tick files or whole directories, or take everything in the scope and narrow it; every rule and ticked file is a removable chip in a selection bar), add extras (each extra's own fields appear only when it is ticked), then preview and download. The result summarizes the build in one line with the download button beside it; the MCP call is folded under it.
- The human workflow and an authentication seam in
pgokf-web. With a writer connection (OKF_PG_WRITER_URL) and an authentication mode (OKF_WEB_AUTH:headerfor an authenticating reverse proxy whose identity headers are believed only from its own addresses, orusersfor people kept in the catalog (Argon2id hashes) with a login form and a signed session cookie), people work on content bundles by role: uploaders add Markdown documents (stamped withgenerated/authorashuman:<name>when absent), editors change or delete them with a validating preview (an edit sets aside earlier verifications and names the editor ingenerated), and approvers work a review queue, recording averifiedevent (the document becomes human-reviewed) or sending a draft back with a note. Every decision is an ordinary OKF field in the document, averifiedlist typed into an upload or edit is set aside rather than believed, roles are a ladder (viewer, uploader, editor, approver, admin), state-changing requests are refused across sites, and the catalog must keep sources (store_source).pgokf-web user addmakes the first admin; the compose stack gained the matchingOKF_UI_*settings. OKF_WEB_AUTH=oidc: the UI as its own OAuth client. A fourth implementation of the identity seam signs people in against any OpenID Connect provider (Entra ID, Okta, Keycloak, Auth0, Google, GitLab) with the authorization code flow and PKCE: the provider's configuration is read from its issuer and must declare it,state,nonce, and the PKCE verifier live in one short-lived signed cookie rather than in memory, the code is exchanged directly over TLS, and the ID token must be signed by a published key with an asymmetric algorithm (noneand HMAC are refused), for the configured issuer and this client, unexpired, and carrying the nonce this site sent. Roles come from a groups claim through the sameOKF_WEB_AUTH_ROLE_MAPthe proxy mode uses, and sign-out ends the provider's session too where it offers one. The session cookie now names the mode that opened it, so a server reconfigured from one mode to another does not honour the old sessions.- Once identities are on, nobody reaches the site without signing in
(login page, static assets, and health probe excepted). Everyone signed in
has a profile page (identity, what the role allows, documents produced and
verified, a password change in
usersmode). The admin role has an admin page: people (add, change role, reset password, remove - each one statement againstpgokf_web.users, in effect at once) and bundles (register a directory bundle, refresh, enable or disable, retire or bring back, unregister). WithOKF_WEB_BUNDLES_DIRpointing at the directory the database reads (mounted read-write), editors change documents of directory bundles in place, written atomically and confined to the bundle, and the bundle is refreshed; the compose stack mounts the bundles into the UI for it. pgokf-mcp --http: the MCP server over HTTP, with tokens and roles. The same JSON-RPC messages the stdio transport carries, served atPOST /mcpfor clients that cannot launch a subprocess (a hosted agent, a fleet sharing one connection to the catalog). One implementation answers both transports, so they cannot drift apart; the only difference is who is asking. It is the Streamable HTTP transport without the parts a request/response server does not need: the server never speaks first, so it opens no event stream and issues no session id, andGET/DELETEanswer405.initializeanswers with the revision the client asked for (2024-11-05,2025-03-26,2025-06-18) and batches are refused. Over stdio the client already holds the connection string and there is nothing to authenticate; this endpoint is reachable, so it is never open. Every request carries a bearer token minted on the web UI's Admin page (or withpgokf-web mcp-token mint) and kept in the catalog as a SHA-256 digest (pgokf_web.mcp_tokens,pgokf_writeronly): the server, a reader, hashes the token a request presents - read only from theAuthorizationheader, and only if it has the shape a minted token has - and askspgokf.mcp_token_bearer(digest), aSECURITY DEFINERlookup that answers for one digest and lists nothing, so the token never travels to the database and a reader learns nothing about tokens it does not hold, and a token minted for another tenant is refused. The check runs before the body is read and before a request takes a working slot, on a connection of its own so it never waits behind a tool call, bounded in number and in wait, so an anonymous caller can neither queue nor buffer anything and a flood of wrong tokens costs the catalog a fixed amount; revoking a token takes effect with the next request, since nothing is cached; the server proves the lookup exists before it opens a socket; refusals and outages are summarized into the log once a second rather than written per request; andGET /healthzanswers 503 while either catalog connection, neither of which is re-established, is unwell. The token's role is the single decision point for what it may reach —readersearches and reads,buildermay also build workspace plugins,writermay also write documents,adminmay also manage bundles — andtools/listfilters by the same answertools/callenforces, showing neither a tool (-32001if called) nor an argument the caller may not use; a tool that does not exist is reported as unknown rather than as a refusal. A request carrying a browserOriginis refused unless the operator named it in--allowed-origins, which is the defence against a page in someone's browser reaching a server on their network, and a named origin gets the CORS answers a browser needs. Arguments that act on the server's own filesystem are marked in the tool schemas and refused over the network, so the next one is refused the day it is added. Bodies, their arrival, request concurrency, request time, and catalog statements are all bounded; TLS belongs in front of it, and the server says so if it binds an address reachable from elsewhere. New compose profilemcp-httpwithPGOKF_MCP_BIND,PGOKF_MCP_PORT, andOKF_MCP_ALLOWED_ORIGINS.- An agent can contribute, under the same rule a person contributes
under:
writerandadminMCP tokens. The role ladder gains two rungs abovebuilder. Awritertoken maylist_bundles,put_document(add or replace one Markdown document in a content bundle) anddelete_document; anadmintoken may alsocreate_content_bundle,refresh_bundle, andset_bundle_state(enable, disable, retire, bring back). They need the server to hold apgokf_writerconnection of its own (--writer-url/OKF_PG_WRITER_URL); without one their tools are still listed - so an agent discovers them and is told plainly that this endpoint does not write - and every call says so. What an agent writes arrives unverified. Whatever the document claims underverifiedis set aside undersuperseded_verifications, with who set it aside and why, andgeneratednames the token asagent:<token name>, so the trust tier the extension derives is neverhuman-reviewedon an agent's say-so and the document lands in the web UI's review queue for a person. That is not a second implementation of the rule: the frontmatter operations moved intopgokf-companion(featuredocuments), and the web UI's upload and edit paths and this tool now run the samecontribute_new/contribute_edit. A write is a full snapshot of the bundle, so it is serialized in the process and refused when the catalog does not keep document sources (store_source), rather than silently dropping what it could not read back. Two things are deliberately not exposed and stay with a person:unregister_bundle, which deletes a bundle's concepts irreversibly, and registering a filesystem bundle, whose path is the database server's and an operator's to choose. - Built plugins can point at that endpoint. The workspace injector's
mcpcomponent takes anmcp_url(the MCP tool'sbuild_workspace_pluginargument, and a field on the web Plugins page beside the MCP command, which is its alternative): instead of configuring a server the harness starts, the entry describes the remote one it calls. Each harness's remote form was read from its own documentation and is recorded with its source, as every layout in the registry is:type: httpwithurlandheadersfor Claude Code, Gemini CLI and Copilot, bareurl/headersfor Cursor, Kimi and Hermes,urlwithbearer_token_env_varfor Codex, andtype: streamable-httpfor a portable Agent Plugin. The bearer token is a secret, so it is treated exactly as the connection string is - referenced, never written:${OKF_MCP_TOKEN}where the harness expands one,${env:OKF_MCP_TOKEN}for Cursor, the variable named for Codex, and nothing at all for an Agent Plugin, whose specification forbids a credential in a package (and whose HTTPS-outside-loopback rule the build enforces). A harness that documents header values as literals - the Copilot CLI and Kimi - does not get an entry in its own configuration file at all: the build writes a fragment with a placeholder and the guide names the file to merge it into and says to keep that out of version control, so no file in the tree is ever meant to hold a secret. A URL carrying credentials, a query string, or a fragment is refused,mcp_urlandmcp_commandare alternatives, the manifest records the endpoint only when an entry was written for it, and the guide explains that areadertoken is offered five tools and abuilderseven.list_plugin_targetsnow reports each target's remote form, so an agent can see whatmcp_urlwould do before calling it. - Two layouts in the registry were corrected while reviewing that work,
and they change the stdio builds too: Hermes Agent documents
${VAR}references in any string value of a server entry, so its snippet now references${OKF_PG_URL}instead of carrying a placeholder connection string; and Kimi reads only~/.kimi/mcp.json, with no project-level file, so its entry is written asokf-mcp.jsonto merge rather than as.kimi/mcp.jsonthe harness would never have read. - GitHub Copilot is a target (
copilot:.github/skills/, MCP entry in.github/mcp.jsonas a typedlocalserver that inherits Copilot's environment). Custom agents:target: customwith a harness description (name, kind, skills directory) builds for an agent the registry does not know, laid out like the kind's base profile; the web builder adds one when a typed name is not in the list, and the MCP tool takesharness. Selections gainedall(start from everything visible, so a rule narrowed only by types, tags, or a query needs no bundle). - The UI is laid out for phones: scrolling navigation and tab strips, tables that scroll inside their panels, results before filters, the builder's target list folded behind its summary, and graph controls on the canvas.
- The OpenAI-compatible embeddings client moved from
pgokf-embedintopgokf-companion(featureembeddings), shared withpgokf-web;pgokf-pgconnexposesparse_configandrustls_connectorfor pool builders.
Changed¶
- Package bytes are stored whatever
store_sourcesays. The setting governs whether a document's source is kept; a package'sSKILL.md, scripts, references and assets are the content, so they are kept regardless andget_skill/get_script/get_referencealways return them. - A session cookie names the mode that issued it, so a server reconfigured from one identity mode to another does not honour sessions opened under the old one. Everyone signs in again after such a change.
- Everything under a package's
scripts/,references/orassets/belongs to that package, whatever it is called. A reserved name (index.md,log.md) there is an ordinary resource, and aSKILL.mdthere is a resource too rather than a second package - which used to take every file beside it out of the enclosing package, silently. A bundle with such a file gains members on its next refresh. - Two registry layouts were corrected, which changes the trees built for
them: Hermes Agent documents
${VAR}references in a server entry, so its snippet references${OKF_PG_URL}instead of carrying a placeholder connection string; and Kimi reads only~/.kimi/mcp.json, so its entry is written asokf-mcp.jsonto merge rather than as a.kimi/mcp.jsonit would never have read. - New ceiling
pgokf.max_bundle_bytes(1 GiB,SIGHUP) bounds a bundle's total discovered size. The per-file and file-count ceilings multiply out to far more than one sync can hold, and a package resource is now kept whole whatever its type. A bundle over the total is refused where it previously registered; raise the setting or narrow the includes. - The web UI is read-only until a writer connection and an identity mode are configured; with both, the human workflow and the admin page are on. It was read-only in every configuration before this release.
Fixed¶
- Building a BM25 index quoted the
default_text_search_configvalue with a backslash-escaped quote (\'), which PostgreSQL refuses whenbackslash_quote = off, and a trailing backslash could run a value on into the statement whenstandard_conforming_strings = off. The literal is now built the way the server's ownquote_literal()builds it - the quote doubled, the backslash escaped - which is correct under every setting. (Affects the BM25 backend shipped since 0.1.15.)
Security¶
- The
okf.shhelper a plugin build generates put the web URL inside a${VAR:-...}default branch, which a shell expands - so a$(...)in that URL ran as a command on whoever ran the helper, and the URL is a free-text build argument reachable from the builder. The value is now a single-quoted assignment, and the validator refuses shell metacharacters. - A bundle file was re-read by path after discovery with no size ceiling
and no link check, so a file could grow past
max_file_bytesor be replaced by a symbolic link between the scan and the read - and a package resource is stored verbatim and served back byte for byte. The read is judged through the descriptor it opens and is capped. - Uploading into an existing bundle name through the "new bundle" path called a full-snapshot resync with only the files in hand, deleting everything else in that bundle. It is refused. Replacing an existing document now needs the editor role, as the ladder always said.
- A disabled bundle stayed readable by direct URL: the source read, and the
bundle page's own concept listing, omitted the
enabledflag that search and browsing applied. - Sign-in ran unbounded Argon2id verifications and threw its throttle on the
user name alone, so an unauthenticated flood could exhaust memory and CPU
and a stranger could hold any named account shut. Verifications are
bounded; the throttle keys on the client address too, refuses an
over-long name unheard, and is capped in size. Behind a reverse proxy the
client address is taken from a trusted
X-Forwarded-Forhop (OKF_WEB_AUTH_TRUSTED_PROXY), so one attacker no longer shares a key with everyone arriving through the proxy. - The directory-bundle editor wrote its temporary file with a call that followed a symbolic link, so a link planted at that path could redirect an edit outside the bundle. The temporary is now created fresh, refusing any link, as the workspace tree writer already did.
- An OpenID Connect login mapped by the
emailclaim accepted an unverified address, so an IdP account carrying someone else's email could assume their actor. Anemail-derived identity now requiresemail_verified. - Signing out only cleared the cookie in that one browser: a session cookie
was a signed value the server could verify but not forget, so a copy taken
beforehand kept working until it expired, and nothing could end a session
early - not even for a person disabled at the identity provider. Every
issued session is now recorded in the catalog (
pgokf_web.sessions), and a cookie the catalog does not list is refused. Signing out ends that session everywhere, the profile page offers sign out everywhere, the admin page lists who holds live sessions and can end anyone's in either mode, and a changed password or a removed person ends theirs. A session is a lever, not a detector: a copied cookie works until its session is ended or expires. - The web UI's people, sessions, MCP tokens, and identity providers live
in the catalog. Four extension-owned tables,
pgokf_web.users,pgokf_web.sessions,pgokf_web.mcp_tokens, andpgokf_web.identity_providers, hold theusersmode's people, every live session of theusersandoidcmodes, the digests of the bearer tokenspgokf-mcpaccepts over HTTP, and the identity providers an admin set up; they are granted topgokf_writeronly (a reader never sees a hash, a session identifier, which tokens exist, or a provider's settings), transactional, shared by every UI instance, and carried bypg_dump. An admin sets identity providers up on the Admin page inusersmode - any number, each with a name, the issuer, client id and secret, the callback URL, claims, and the group-to-role map - and the sign-in page then offers one "Sign in with …" button per provider below the password form; saving reaches the provider first, so a wrong issuer is refused before it is stored; a provider is known by a slug made from its name (okta,github) that its sessions and the people it brought carry, so switching it off, removing it, or changing its issuer or client ends the sessions it opened and no other (and re-registering it puts its people back at the bottom of the ladder), and every UI instance picks a change up on its next sign-in. A provider may be any OpenID Connect provider, or GitHub (github.com or an Enterprise Server), which speaks OAuth but not OpenID Connect: the same code flow with PKCE yields an access token, and the person comes from GitHub's API (subthe numeric account id,login,name, the primary verifiedemail) with their organizations andorg/teamslugs as groups. A callback URL may be plainhttp://on the loopback interface or a private address, where the site is already served that way. A person a provider signs in appears under People at their first sign-in, with the name the provider reports, without a password and at the bottom of the ladder; an admin can set their role there, and the higher of that role and the group-mapped one applies on every request (pgokf_web.usersgainsdisplay_nameandprovider, and aNULLpassword hash for them, which refuses a password sign-in under their name; a name belongs to exactly one way in - a provider never signs in a password person's name nor a name another provider brought - and no password can be set for a provider person). The Admin page is tabs now - People, Identity providers, MCP tokens, Bundles, Catalog settings - and People shows everyone by name with their sign-in name beneath, searches by either, and pages (25 to 200 to a page), so a site with hundreds of people stays usable; a password person may be given a name to show (pgokf-web user add --display, or the form). The client secret is stored sealed (AES-256-GCM under a key derived fromOKF_WEB_SESSION_SECRET), the table refuses anything but the sealed form, and without a session secret of its own the UI keeps no client secret (a public client with PKCE still works). The env-configuredoidcmode remains for a site with no local people at all. The Admin page mints a token - shown once, on the page that minted it, withCache-Control: no-storeand never in a URL - and revokes one;pgokf-web mcp-token mint|list|revokedoes the same from a shell for a stack without the UI. A token is minted for the tenant the UI serves, and an MCP endpoint admits only tokens minted for its own. The UI reaches all three through a pool of its own on the writer URL - never shared with the human workflow's long resyncs, with a short statement budget, and probed at startup - so theusersandoidcmodes requireOKF_PG_WRITER_URL. A catalog that cannot answer an identity lookup is a 503, never a wrong password or a sign-out.pgokf-web user add/user set-password(the password read from standard input) make and rescue people; the Admin page does the rest. - Security headers, the Content-Security-Policy included, now reach the responses the authentication and same-site layers produce themselves.
- A built tree is written in full or not at all, cannot hold two paths that
are one file on a case-insensitive filesystem, and cannot carry a segment
that Windows would normalize into
... The lockfile hashes every file of the tree, the generated ones included.
0.1.16 - 2026-09-05¶
Deny-by-default tenancy, on demand. The new durable policy key
require_tenant (default false) makes a session that has not set
pgokf.tenant see nothing and refuse to ingest, instead of the see-all
behavior every earlier release had. Nothing changes until an administrator
turns it on; ALTER EXTENSION pgokf UPDATE TO '0.1.16' adds one column and
rewrites the thirteen tenant-isolation policies in place.
Added¶
require_tenantpolicy key and the reader-levelpgokf.tenant_required()function every row-level-security policy consults (as an uncorrelated sub-select: one evaluation per statement, never a per-row call). With the policy on, an unscoped session sees no rows through the policies and every reader built on them, theSECURITY DEFINERreaders apply the same rule (health()counts,list_sync_log,list_sync_changes,list_access_log, and the ParadeDBbm25_hitspath come back empty;get_concept_sourceraises the same not-found22023a foreign tenant gets), and the ingestion tier, the bundle-addressed writers and exports, andpurge_retiredrefuse it with SQLSTATE42501.set_config/reset_confignever need a tenant, so the policy can always be turned off again.health()gainstenant_required. The function is executable by any role withUSAGEon schemapgokf, because the policies depend on it.- Companions:
pgokf-ingestaccepts--tenant/OKF_TENANTlikepgokf-embedandpgokf-mcp; all three apply it through one sharedpgokf-pgconnhelper. The compose stack passesOKF_EMBED_TENANTandOKF_INGEST_TENANTthrough (next to the existingOKF_MCP_TENANT).
Changed¶
schedule_refreshpins the bundle's tenant into the pg_cron job command (set_config('pgokf.tenant', ...)beforerefresh_bundle), so the cron worker's own, tenant-less session satisfies the tenant rules. Jobs scheduled by earlier releases run the bare call; re-schedule them (schedule_refreshupdates a job in place) before turningrequire_tenanton, or pin the job role's tenant withALTER ROLE ... SET pgokf.tenant.- The write-side tenant rule (
enforce_bundle_tenant) now also refuses an unscoped session when a tenant is required, with a distinct42501message naming the fix, rather than the unknown-bundle22023used for cross-tenant ids.
0.1.15 - 2026-09-05¶
A PostgreSQL-licensed BM25 backend. The bm25 search backend now runs on
a selectable provider, and Tiger Data's pg_textsearch (PostgreSQL license,
PostgreSQL 17 and 18) joins ParadeDB's pg_search. The Docker image bundles
pg_textsearch on the 17 and 18 images and no longer bundles pg_search by
default. ALTER EXTENSION pgokf UPDATE TO '0.1.15' adds one policy column
with its default and touches no existing row.
Added¶
bm25_providerpolicy key (auto|pg_textsearch|pg_search, defaultauto, which preferspg_textsearchwhen installed). Both providers name their index access methodbm25and cannot coexist in one database, so resolution is unambiguous.rebuild_search_index()builds the resolved provider's index;search_index_status()reportsbm25.providerandbm25.provider_setting;health().bm25_readyaccepts either provider.pg_textsearchbackend. One expression index over title, description, and body with the catalog's text-search configuration (baked into the index; rebuild after changing it). A page is served by the provider's index-ordered top-k scan (ORDER BY <expr> <@> to_bm25query(...) LIMIT n) with the structured filters, the keyset predicate, and row-level security applied as ordinary quals on that scan - inline with invoker rights, no privileged helper - then orderedrank DESC, bundle_id, concept_idin SQL. Rows tying the page's last rank are read past the page so keyset pages tile exactly, up to 256 tied rows per boundary (beyond that aWARNINGand approximate paging). Query text is plain terms, as it already was onpg_search(neither provider interprets web-search operators). Covered by new in-database tests (the query shape is asserted to plan as abm25index scan) that run whereverpg_textsearchis preloaded, including CI's PostgreSQL 17 and 18 legs.- Image:
WITH_PG_TEXTSEARCH(defaultauto: installed on 17 and 18 from the pinned, checksum-verified release package via the sharedfetch-pg-textsearch.sh, with its PostgreSQL-license notice under/usr/share/doc; skipped elsewhere). CI builds the 17/18 images withWITH_PG_TEXTSEARCH=1and the smoke test requires a provider there, so a stale checksum table fails the build rather than publishing a provider-less image.WITH_PG_SEARCHnow defaults to0; the ParadeDB build path remains available.pgokf-restoreskips apg_textsearchextension entry the target cannot replay, as it already did forpg_search. The compose stack preloadspg_textsearch.
Changed¶
search_index_status().bm25.availableandhealth().bm25_readynow follow the providerbm25_providerresolves to (they were "pg_searchinstalled"), so a pinned provider that is missing shows as unavailable even when the other one is present, and the fallback warning names what is installed instead.concept_search,find_similar, andconcept_search_hybridare nowPARALLEL RESTRICTED(executed in the leader of a parallel plan, never in a worker): the BM25 providers declare their scoring parallel-unsafe, and these functions run it through SPI. Results and signatures are unchanged; the upgrade script relabels the installed functions.- Documentation for search backends, configuration, packaging, and the compose deployment covers provider selection and the licensing difference.
- Compose stack:
shared_preload_librariesnow namespg_textsearchinstead ofpg_search(overridable through the newPGOKF_PRELOADvariable, e.g.pgokf,pg_cronfor a 15/16/19 image), and the ParadeDB-onlyparadedb.planner_warningssetting is gone. An older compose file that still preloadspg_searchwill not start the 0.1.15 image (the library is no longer in it): update the preload line, or build the image withWITH_PG_SEARCH=1 WITH_PG_TEXTSEARCH=0to keep ParadeDB. A database that haspg_searchcreated must drop it (on the old image,DROP EXTENSION pg_search CASCADE) beforepg_textsearchcan be created; the step-by-step migration is in docs/compose-deployment.md. - Test harness: the in-database tests that need a preloaded BM25 provider
now run when
PGOKF_TEST_PRELOADnames it (pgrx starts its own instance, so the operator's cluster configuration never reached them, and they had been skipping silently), and fail instead of skipping when the requested provider is unusable.
0.1.14 - 2026-09-04¶
Complete logical backups, production container packaging, and native
arm64. The one in-database change makes pg_dump actually capture the
catalog; everything else is packaging, deployment, companion, and CI work.
ALTER EXTENSION pgokf UPDATE TO '0.1.14' adds a trigger and the backup
registrations and touches no existing row; a catalog upgraded with it is
identical to a fresh 0.1.14 install.
Fixed¶
- BM25 search works for non-superuser readers. With
search_backend = bm25, any session that is not the table owner - every production reader - gotUnsupported query shapefrom pg_search: row-level security wrapspgokf.conceptsin a security-barrier subquery its custom scan cannot plan. The hit query now runs throughpgokf.bm25_hits, aSECURITY DEFINERhelper (pinnedsearch_path, execute granted topgokf_readeronly) that applies the same explicitpgokf.tenantpredicate the policies enforce, soconcept_searchand the hybrid search keep their invoker-rights contract and the result set is unchanged. Covered by a new in-database test that runs wherever pg_search is preloaded. pg_dumpnow captures the catalog. PostgreSQL skips the contents of extension-owned tables unless the extension registers them withpg_extension_config_dump; pgokf never did, so every archive carried only theCREATE EXTENSIONstatement and a restore came back empty - despite the operations guide describing dumps as complete. Everypgokf.*andpgokf_private.*table and sequence is now registered (discovered from the extension's dependency graph, so future tables are covered automatically), and aBEFORE INSERTtrigger on the singletonpgokf_private.configrow folds the restored policy row into the oneCREATE EXTENSIONseeds instead of failing on a duplicate key. Registered relations and the trigger are asserted by new in-database tests, and the image smoke test restores a real archive.
Added¶
- Server image with the optional extensions built in. The Docker image
now ships pgvector, pg_cron, and ParadeDB pg_search (each a
WITH_*build argument; pg_search is fetched from its pinned upstream release and verified against a committed SHA256 table), aHEALTHCHECK, an OCI version label, and first-init hooks that create the extensions when preloaded, create least-privilege login roles fromPGOKF_{ADMIN,WRITER,READER}_PASSWORD, and apply a JSONPGOKF_POLICYthroughpgokf.set_config. It also carriespgokf-backup(a verifiedpg_dump+ roles-dump tool with retention) andpgokf-restore(a single-transaction restore that skips the archive entries an initialized target cannot replay: pg_search's unownedparadedbschema and, for a differently named database, pg_cron's objects). - Companions image (
Dockerfile.companions):pgokf-ingest,pgokf-embed, andpgokf-mcpin one non-root image. - Multi-architecture images. CI builds and smoke-tests both images
natively on amd64 and arm64 runners and merges them into one manifest per
tag (
ghcr.io/logicocean/pgokf:<version>-pg<major>,ghcr.io/logicocean/pgokf-companions:<version>), so they run on x86 and arm64 servers and on Apple Silicon. The.debmatrix gained arm64 legs. - Reference compose deployment (
deploy/compose/, documented indocs/compose-deployment.md): server, embedding daemon, cron-driven backups, and optional ingestion and MCP services, all configured from one.env. pgokf-embed --watch/--interval: the embedder runs as a daemon that embeds newly registered or refreshed concepts every interval, reconnecting per pass and surviving transient outages. The loop and its SIGINT/SIGTERM shutdown live in the newpgokf-companioncrate, shared withpgokf-ingest(which therefore now also stops cleanly on SIGTERM, i.e.docker stop).- Packaging lint in CI (hadolint, shellcheck,
docker build --check, compose render), a.dockerignore, and reusable smoke scripts (packaging/docker/smoke-test.sh,smoke-test-companions.sh).
Changed¶
- The image's first-init SQL creates
vectorwhenever present andpg_search/pg_cronwhen preloaded, instead of onlypgokf. - Documentation: new compose deployment guide; packaging, operations, search, and deployment-topology guides updated for the bundled extensions, the backup tool, the embedder daemon, and multi-arch publishing.
0.1.13 - 2026-08-29¶
Relicensed to AGPL-3.0 plus commercial, and the first public release. This
release also fixes a set of audited defects in the shipped catalog and
companions. The in-database SQL surface is
unchanged from 0.1.12 - no table, type, function signature, index, grant,
comment, role, or configuration key is added, dropped, renamed, or rewritten, so
ALTER EXTENSION pgokf UPDATE TO '0.1.13' is a documented no-op that yields a
catalog identical to a fresh 0.1.13, with every bundle, concept, embedding, link,
history version, and provenance record intact. Every fix is internal code, a new
input validation, companion behavior, or documentation; loading the 0.1.13
shared library is what activates the corrected code paths. api_stability and
the no-data-loss upgrade guarantees are preserved.
Changed¶
- Relicensed to dual AGPL-3.0 + commercial (previously MIT). Every crate in
the workspace - the extension, the
okf-parser/okf-synclibraries, and the companion tools (pgokf-ingest,pgokf-embed,pgokf-mcp,pgokf-pgconn) - is nowAGPL-3.0-only, and a commercial license is available for use the AGPL does not permit. SeeLICENSING.mdandCOMM-LICENSE.md.
Security¶
- Denial-of-service in
concept_neighborsclosed (HIGH). The recursive traversal previously enumerated every simple path from the seed (≈O(N^hops)), so a reader callingconcept_neighbors(seed, 5)on a dense bundle could spin the backend on millions of walk rows for a tiny answer. The traversal is rewritten as a set-based, cycle-safe breadth-first search that records the first (minimum-hop) visit of each neighbor and never re-expands a visited node -O(V + E)work - returning identical results (distinct neighbors, shortest hop distance, cycle-safe, active-bundle-scoped) for normal graphs. A denseK30bundle that formerly timed out now answers in milliseconds. - Embedding poisoning rejected at write (HIGH).
set_concept_embeddingnow validates that every element of the supplied vector is finite, raising SQLSTATE22023(naming the offending index) for aNaN/Infinityelement before the upsert. Storage isreal[], so such a value inserted silently but was rejected by pgvector at every query/index cast - one bad write could break semantic and hybrid search andrebuild_embedding_indexcatalog-wide until the row was found and fixed. purge_retireddata-loss race closed (HIGH).purge_retiredsnapshotted the eligible bundles and then hard-deleted each without re-checking, so a concurrentunretire_bundlethat committed in between could have its restored bundle (and its concept history) silently deleted. The per-bundle delete now re-evaluates the eligibility predicate (retired_at IS NOT NULL AND retired_at < now() - older_than) atomically under the bundle advisory lock, skipping - never deleting - a bundle that is no longer eligible.- Multi-tenancy trust model documented honestly.
docs/security.mdanddocs/multi-tenancy.mdnow state plainly that thepgokf.tenantGUC is a scoping selector, not a hard security boundary against a tenant who can run arbitrary SQL (any session canSET/RESETit, and a pinnedALTER ROLEdefault is overridable in-session), that the unset default fails open to see-all, and that a hard boundary requires a constrained access layer that pins the GUC or a per-tenant-role model.
Fixed¶
log.mdmidnight-timestamp corruption (MED). A space-separatedYYYY-MM-DD HH:MM[:SS]leading timestamp in a reservedlog.mdentry parsed to midnight (only the first whitespace token was read). It now parses to the real instant, while an untimestamped line still yields aNULLlogged_atand the entry text stays lossless.concept_neighborsNULL-bundle disambiguation counted inactive bundles (LOW). With nobundle_id, disambiguation now counts only active bundles (enabled AND retired_at IS NULL), so a disabled/retired duplicate of a concept id no longer raises a spurious22023that blocked the only active bundle.- Self-linked seed excluded from its own neighbor set (LOW). A concept that links to itself is no longer returned as its own neighbor.
- Deterministic tiebreak for semantic/hybrid search (LOW).
concept_search_semanticandconcept_search_hybridnow break equal distance/fused-score ties on(bundle_id, concept_id)for a stable order.
Companions¶
- Optional TLS to PostgreSQL for all three companions (MED).
pgokf-ingest,pgokf-embed, andpgokf-mcpnow share a smallpgokf-pgconnhelper crate and accept a--tlsflag (envOKF_PG_TLS); TLS is also enabled bysslmode=requirein the connection string. When enabled, the link usesrustls(reusing the stack already pulled byobject_store/reqwest, socargo denystays green) and verifies the server certificate against the platform trust store.NoTlsremains the default for a local socket / trusted network. pgokf-ingestunit tests. Added AAA unit tests for the companion's path-derivation (prefix strip) and change-detection (content-hash) helpers.
Internal¶
- New
crates/pgokf-pgconnworkspace crate centralizes the companions' PostgreSQL connect step. Version bumped to0.1.13across the workspace,pgokf.control, andMETA.json; a documented no-oppgokf--0.1.12--0.1.13.sqlupgrade script ships. Regression tests (Rust unit tests and in-database#[pg_test]s) cover every fix above.
0.1.12 - 2026-08-28¶
Companion tooling: three new out-of-process binaries that pair with the
already-shipped catalog surface. The in-database extension is functionally
unchanged from 0.1.11 - no table, type, function, index, grant, comment, or
configuration key is added, dropped, renamed, or rewritten, so
ALTER EXTENSION pgokf UPDATE TO '0.1.12' is a documented no-op that yields a
catalog identical to a fresh 0.1.12 with every bundle, concept, embedding,
history version, and provenance record intact. All three tools do their network
and credential handling outside PostgreSQL and reach the catalog only
through its public SQL functions, preserving the extension's no-network-I/O
guarantee.
Added¶
pgokf-embed(newcrates/pgokf-embed) - the reference embedding generator that pairs with the shipped semantic search. A standalone async binary that connects as apgokf_writerrole, finds concepts inpgokf.conceptswith no matchingpgokf.concept_embeddingrow (optionally scoped by--bundle), builds a boundedtitle + description + body_texttext per concept, calls a configurable OpenAI-compatible/v1/embeddingsendpoint (POST {endpoint}/v1/embeddingswith{model, input}and aBearertoken) in batches, and streams each returned vector back throughpgokf.set_concept_embedding(bundle_id, concept_id, embedding). The endpoint, model, and API key are supplied by CLI/env and are never stored in PostgreSQL or hard-coded; the target dimension comes frompgokf.get_config(embedding_dim) or--dim. Any OpenAI-compatible server works - OpenAI, a localtext-embeddings-inference/llama.cppserver, or a mock.pgokf-mcp(newcrates/pgokf-mcp) - a Model Context Protocol server that exposes the catalog to AI agents over stdio JSON-RPC 2.0. A hand-rolled, dependency-light server (no MCP SDK) implementing the MCP handshake (initialize→serverInfo/capabilities,tools/list,tools/call) and four tools backed by the shipped functions:concept_search(query,bundle_id?,limit?, plustype/tags/status/trust_tierfilters),find_similar(concept_id,bundle_id?,limit?),concept_neighbors(concept_id,max_hops?,bundle_id?), andget_concept(concept_id,bundle_id?). Connection string and optionalpgokf.tenantcome from CLI/env.pgokf-ingest --watch- the mountless ingestion companion gains a watch daemon mode (--watch, with--intervalseconds, default 60) that periodically re-lists the object store and re-ingests throughregister_bundle_content(which diffs server-side, so a changed object resyncs on the next pass). A content hash of the collected object set lets an unchanged pass skip the round-trip. One-shot mode (no--watch) is unchanged. Shutdown is graceful onSIGINT.
Changed¶
- The workspace version is
0.1.12; the extension'sdefault_version,META.json, and the runtimepgokf.version()all report0.1.12. The release ships the no-opsql/pgokf--0.1.11--0.1.12.sqlupgrade script.
0.1.11 - 2026-08-28¶
Opt-in concept version history: an append-only SCD Type-2 version trail of
each concept, with point-in-time queries - "what did this runbook say last
Tuesday?". The feature is off by default: with the new track_history key
disabled (the default), a sync records nothing and an existing install behaves
exactly as before with zero extra storage, which is what keeps the release
backward compatible. Everything is additive, so
ALTER EXTENSION pgokf UPDATE TO '0.1.11' migrates an existing install in a
single transaction and yields a catalog identical to a fresh 0.1.11. The two new
config columns are backfilled by their defaults (history off, retention 0).
Added¶
pgokf.concept_historytable - an append-only SCD Type-2 version trail. One row per concept version with a per-concept monotonicversionand a validity interval[valid_from, valid_to)(valid_to IS NULL= the current open version), achange_kind(added/updated/removed), and a snapshot of the concept core (type,title,description,tags,resource,body_text,file_hash) at that version. Populated only whentrack_historyis on. Cascades frompgokf.bundles(notpgokf.concepts), so a removed concept keeps its history until the bundle is unregistered. Multi-tenant with the standard opt-intenant_idrow-level security and a(bundle_id, concept_id, valid_from)lookup index;SELECTgranted topgokf_reader.pgokf.concept_history(bundle_id, concept_id, max_rows DEFAULT 100)- the version timeline for one concept, newest first, asSETOF pgokf.concept_version. Reader-level,STABLE, invoker rights (the caller's tenant RLS applies).pgokf.concept_as_of(bundle_id, concept_id, as_of)- the single version valid at an instant (valid_from <= as_of AND (valid_to IS NULL OR as_of < valid_to)), or zero rows if the concept did not exist or had been removed then. The point-in-time answer. Reader-level,STABLE, invoker rights.pgokf.concept_versioncomposite (version,valid_from,valid_to,change_kind,type,title,description,file_hash) - the row shape both readers return.track_historyconfiguration key (boolean, defaultfalse) - the opt-in switch. When on, every register/refresh/content sync records history from its delta inside the same transaction, so history commits atomically with the sync: an added concept starts at version 1; an updated concept closes its open version and appends the next; a removed concept closes its open version and appends a zero-width removal tombstone. Documented as a storage/retention tradeoff.history_retention_daysconfiguration key (integer, default0= keep indefinitely) - bounds history growth. When positive, closed versions (valid_to IS NOT NULL) older than the window are pruned in the same transaction after each sync; the single current open version of a concept is never pruned.
Compatibility¶
- Backward compatible and opt-in. With
track_historyoff (the default) nopgokf.concept_historyrow is ever written and there is zero behavior or storage change; the new reader functions simply return no rows. Enablingtrack_historyis not retroactive - recording begins at the next sync, and a concept first versioned afterward begins its chain at that sync'schange_kind. - Version-history intervals are contiguous and non-overlapping per concept, with
exactly one open version per live concept; each sync stamps its rows with a
single captured instant so a closed version's
valid_toabuts the next version'svalid_from. - The public function-surface count rises from 36 to 38 (
concept_history,concept_as_of); see docs/api-stability.md.
0.1.10 - 2026-08-28¶
OKF-conformance batch: an Attested Computation concept's type-specific
reference fields now become traversable graph edges, and the reserved per-
directory log.md activity log is now projected instead of dropped. Everything
is additive and backward compatible, so ALTER EXTENSION pgokf UPDATE TO '0.1.10'
migrates an existing install in a single transaction and yields a catalog
identical to a fresh 0.1.10. The one new pgokf.links column is backfilled by
its default.
Added¶
- Attested Computation reference fields as graph edges - for a concept whose
typeisAttested Computation, itscomputation,executor, andattesterreference fields (each a bare resource path or a{resource: …}mapping) are resolved intopgokf.linksas typed internal edges, numbered after the concept's body links.pgokf.concept_neighborsnow traverses them like any resolved internal edge, so the executor/attester/computation concepts are reachable even when the body links to none of them. A missing, external, or dangling reference is retained asis_external/resolved = falseand never traversed, exactly like any other link. Non-attested concepts are unaffected. pgokf.links.link_relation(text NOT NULL DEFAULT 'reference') - a new additive column carrying the edge's semantic relation, distinct from the Markdown-constructlink_kind:referencefor every ordinary link, orattestation:computation/attestation:executor/attestation:attesterfor the new typed edges. Existing rows are backfilled toreference.- Reserved
log.mdprojection - the per-directory OKFlog.mdactivity logs, previously skipped entirely, are now parsed and projected into a newpgokf.bundle_logtable (bundle_id,tenant_id,directory,ordinal,logged_at,entry; PK(bundle_id, directory, ordinal); cascades frompgokf.bundles; opt-in multi-tenant RLS). Each non-blank line becomes one entry, with a leading ISO 8601 timestamp lifted intologged_atand the line stored losslessly. The projection is replaced wholesale on every sync, so it tracks edits/additions/removals; alog.mdis still never a concept and never counts toward the bundle'sfile_count.index.mdhandling is unchanged. pgokf.list_bundle_log(bundle_id bigint, directory text DEFAULT NULL, max_rows int DEFAULT 500)(SETOF pgokf.bundle_log_entry, reader-level,STABLE PARALLEL SAFE, invoker rights) - lists a bundle's log entries ordered by directory then ordinal, optionally scoped to one directory (''for the root). New compositepgokf.bundle_log_entry(bundle_id, directory, ordinal, logged_at, entry). Raises22023whenmax_rows < 0.
0.1.9 - 2026-08-28¶
Search and scheduling batch: keyset pagination and faceted counts on search,
a search-index coverage report, and an optional pg_cron scheduled re-sync.
Everything is additive and backward compatible, so
ALTER EXTENSION pgokf UPDATE TO '0.1.9' migrates an existing install in a single
transaction and yields a catalog identical to a fresh 0.1.9. concept_search
gains one optional trailing argument (documented below); no existing type or
default changes.
Added¶
- Keyset / cursor pagination on
concept_search- a new optional trailing argumentafter_cursor jsonb DEFAULT NULL. Ranked results now have a stable total order (rank DESC, thenbundle_id ASC, thenconcept_id ASC); copy therank,bundle_id, andconcept_idof a page's last row intoafter_cursorand the next page continues strictly after it, with noOFFSETdrift and no duplicates or skips even when ranks tie. Applied in both the native and BM25 backends. A malformed cursor raises22023. The historical three- through seven-argument calls are unchanged (after_cursordefaults to the first page). - Faceted result counts -
pgokf.search_facets(query, bundle_id DEFAULT NULL, facet DEFAULT 'type', concept_type DEFAULT NULL, tags DEFAULT NULL, status DEFAULT NULL, trust_tier DEFAULT NULL)(SETOF pgokf.search_facet, reader-level) counts the same matching setconcept_searchwould, grouped by one facet -type,bundle,status,trust_tier, ortag(any other value raises22023; the facet is dispatched on, never interpolated). Thetagfacet counts a concept once per tag. New compositepgokf.search_facet(facet_value text, count bigint). - Search-index health / coverage -
pgokf.search_index_status()(jsonb, reader-level) reports the configured backend, that native FTS is always available, and for each optional index whether its extension is installed, whether the index exists, and how much of the catalog it covers (BM25 rows and embedding-vector coverage vs. total concepts). Coverage counts are tenant-scoped. - Optional
pg_cronscheduled re-sync -pgokf.schedule_refresh(bundle_id, schedule)(text, admin-tier) registers an idempotentpgokf_refresh_<bundle_id>cron job runningSELECT pgokf.refresh_bundle(<id>)on the given schedule, andpgokf.unschedule_refresh(bundle_id)(boolean, admin-tier) removes it. The coupling topg_cronis runtime-only (mirroring the pgvector /pg_searchoptional-dependency seam):CREATE EXTENSION pgokfsucceeds withoutpg_cron, and when it is absentschedule_refreshraises a clear22023naming the missing dependency whileunschedule_refreshis a clean no-op. Full scheduling requirespg_croninshared_preload_libraries.
Changed¶
concept_searchresult order is now a stable total order (rank DESC, bundle_id ASC, concept_id ASC), replacing the previousrank DESC, concept_id ASC. This only refines the tiebreak for equal-rank hits and is what makes keyset pagination exact.- The
0.1.8 → 0.1.9upgrade replaces the seven-argumentconcept_searchoverload with the eight-argument superset (DROPold +CREATEnew, in one transaction), exactly as0.1.5 → 0.1.6did, so an upgraded catalog carries a singleconcept_searchoverload identical to a fresh install.
0.1.8 - 2026-08-28¶
Lifecycle and audit batch: a per-sync change manifest, a reversible bundle
retirement window, an exfiltration/access audit, and cross-bundle content
deduplication. Everything is additive and backward compatible, so
ALTER EXTENSION pgokf UPDATE TO '0.1.8' migrates an existing install in a single
transaction and yields a catalog identical to a fresh 0.1.8. Six new public
functions are added; no existing signature, type, or default changes.
Added¶
- Per-concept change manifest - every
register/refresh/contentsync now records which concepts it added, updated, or removed, not just the aggregate counts. Stored in the new administrator-onlypgokf_private.sync_log_change(a child ofpgokf_private.sync_log, cascading on delete so it shares thesync_log_retention_dayswindow) and read through the reader-levelpgokf.list_sync_changes(sync_id, max_rows DEFAULT 1000)(SETOF pgokf.sync_change), tenant-scoped likelist_sync_log. - Bundle retirement / soft-delete window - a new
bundles.retired_attimestamp and three functions:pgokf.retire_bundle(bundle_id)andpgokf.unretire_bundle(bundle_id)(writer-tier), andpgokf.purge_retired(older_than interval DEFAULT '7 days')(admin-tier). A bundle is active only whenenabled AND retired_at IS NULL; a retired bundle is excluded fromconcept_search,concept_neighbors, semantic/hybrid search, and the defaultlist_bundleswithout deleting any rows, so retirement is a reversible undo window for the hardunregister_bundlecascade.purge_retiredhard-deletes bundles retired longer than the interval (writing oneunregisteraudit row each). Retirement is idempotent (re-retiring keeps the original instant) and does not touchenabled. - Exfiltration / access audit - the three content-exporting operations
(
export_parquet,export_sources,get_concept_source) now each append one row to the new administrator-onlypgokf_private.access_log(who read/exported what, and when), read through the admin-tierpgokf.list_access_log(bundle_id DEFAULT NULL, max_rows DEFAULT 100)(SETOF pgokf.access_log_entry). The log shares thesync_log_retention_daysretention window. - Cross-bundle content deduplication -
pgokf.duplicate_concepts(bundle_id DEFAULT NULL, min_group int DEFAULT 2)(SETOF pgokf.duplicate_group, reader-level) groups byte-identical concepts by their stored BLAKE3file_hash, so an operator can find the same runbook or reference copied across bundles. retired_aton thepgokf.catalog_statcomposite (returned bycatalog_stats), so retired bundles - hidden fromlist_bundles- stay visible with their retirement instant.
Changed¶
pgokf.get_concept_sourceis nowSECURITY DEFINERand tenant-scoped (so it can append its access-audit row); its reader-tier grant and signature are unchanged.pgokf.list_bundlesnow excludes retired bundles by default (retired bundles remain reachable by id viabundle_infoand visible incatalog_stats); disabled-but-not-retired bundles are still listed.- The
sync_log_retention_dayspolicy now also governspgokf_private.access_logand, transitively, the change manifest (via thesync_log_changecascade).
0.1.7 - 2026-08-28¶
Opt-in multi-tenant isolation, built from a per-session GUC and PostgreSQL
row-level security. Everything is strictly backward compatible: an existing
install, and any session that never sets a tenant, sees all rows and behaves
exactly as under 0.1.6, so ALTER EXTENSION pgokf UPDATE TO '0.1.7' migrates an
existing install in a single transaction and yields a catalog identical to a
fresh 0.1.7 (every existing row backfills to the default tenant). No public API
surface changes - no new functions, types, or arguments.
Added¶
- Denormalized
tenant_id(text NOT NULL DEFAULT 'default') on every projection table -bundles,concepts,concept_metadata,links,concept_provenance,concept_verification,concept_provenance_source,concept_source,concept_embedding- and onpgokf_private.sync_log. Indexed where it helps (a dedicated index onconcepts; onbundlesthe newUNIQUE (tenant_id, path)index already leads with it). pgokf.tenantGUC (USERSET, empty default) - the per-session tenant selector. Set it per session (SET pgokf.tenant = 'acme'), per login role (ALTER ROLE r SET pgokf.tenant = ...), or as a connection option; empty (the default) means the session declares no tenant and sees every row.- Row-level security on every projection table with an opt-in-by-usage
policy: a session that has not set
pgokf.tenantmatches all rows (backward compatible), a session that has set it matches only that tenant. RLS is enabled but not forced, so theSECURITY DEFINERwrite/admin functions bypass it to stamp and read within one single-tenant bundle. docs/multi-tenancy.mddocumenting the model, the per-tenant bundle keys, theSECURITY DEFINER-bypass reasoning, and the strict-isolation contract.
Changed¶
- Per-tenant bundle registration key.
pgokf.bundlesis now keyedUNIQUE (tenant_id, path)instead ofUNIQUE (path), so two tenants may register the same filesystem orcontent:<name>path as independent bundles. The duplicate-registration23505check is scoped to the current tenant. (The upgrade replaces the old single-column key with this strict superset; no data is touched.) - Writes stamp the tenant.
register_bundle/register_bundle_contentstamp the bundle row fromeffective_tenant(); every projected child row and theset_concept_embeddingrow inherit the bundle's tenant; thesync_logrow records the operating tenant.refresh_bundle,unregister_bundle, andset_bundle_enabledoperate on an existing bundle and never change its tenant. list_sync_logandhealthare tenant-scoped. Both areSECURITY DEFINER(they bypass RLS), so they apply the same opt-in tenant filter explicitly:list_sync_logfilters its rows andhealth'sbundle_count/concept_countare scoped, each a no-op for an unset session.
0.1.6 - 2026-08-28¶
An additive search-enhancement batch: structured filters on ranked search, a
content more-like-this, and an optional pgvector semantic / hybrid surface.
Everything is backward compatible - the historical concept_search(query,
bundle_id, limit_count) call is unchanged - so ALTER EXTENSION pgokf UPDATE TO
'0.1.6' migrates an existing install in a single transaction and yields a
catalog byte-identical to a fresh 0.1.6 (verified by diffing the two).
Added¶
- Structured filters on
pgokf.concept_search. Four optional trailing arguments, each a no-op whenNULL:concept_type text,tags text[](ALL-of containment - a hit must carry every listed tag),status text, andtrust_tier text(matched againstpgokf.concept_provenance). The filters are parameter-boundANDclauses applied in both the native and BM25 backends, reusing the existingtags,type, and provenance indexes. pgokf.find_similar(concept_id text, bundle_id bigint DEFAULT NULL, limit_count int DEFAULT 10)- content more-like-this. It extracts a seed concept's most salientbody_tsvlexemes and ranks other concepts against them through the configuredsearch_backend(native FTS or BM25), excluding the seed. Distinct fromconcept_neighbors(the authored link graph).- Optional semantic + hybrid search via pgvector (mirroring the optional
BM25 seam exactly -
CREATE EXTENSION pgokfstill succeeds without pgvector): pgokf.concept_embeddingstores per-concept vectors as the builtinreal[](never avectorcolumn, so the extension takes no static pgvector dependency), cast tovector(embedding_dim)only at query and index time.pgokf.set_concept_embedding(bundle_id, concept_id, embedding real[])(writer-tier) is how a companion embedder streams caller-computed vectors in; the extension never computes embeddings or performs network I/O.pgokf.concept_search_semantic(query_embedding real[], …)ranks by pgvector cosine distance; therankcolumn is the normalized cosine similarity. It requires pgvector and raises22023naming the missing dependency when it is absent (semantic search has no lexical fallback).pgokf.concept_search_hybrid(query text, query_embedding real[], …)fuses the lexical and semantic results with Reciprocal Rank Fusion (RRF, k = 60) entirely in SQL. When pgvector is absent it degrades to lexical-only with aWARNING.pgokf.rebuild_embedding_index()(admin-tier, mirroringrebuild_search_index) builds a pgvector HNSW cosine index for the configured dimension; a logged no-op when pgvector is absent or the dimension exceeds pgvector's 2000-dim HNSW limit.- New config key
embedding_dim(integer, default 1536) governs the expected embedding length and the HNSW index typmod.
Changed¶
pgokf.concept_searchgained the four trailing filter arguments (a new function identity inpg_proc). The upgrade script removes the superseded three-argument overload and creates the seven-argument one, so an upgraded catalog carries exactly oneconcept_searchoverload - identical to a fresh install - and every historical one-, two-, and three-argument call still resolves through the new defaults.
0.1.5 - 2026-08-28¶
An additive audit, lifecycle, and observability batch. Everything is
backward compatible - a new admin-only table, three composite types, five new
functions, three new configuration keys, and two functions whose behavior
gained a filter - so ALTER EXTENSION pgokf UPDATE TO '0.1.5' migrates an
existing install in a single transaction (the 0.1.4 → 0.1.5 upgrade script
adds the sync_log table, the three types, the five functions, and the two
config columns; the rest lives in the shared library and activates on load).
Added¶
- Sync/audit log. A new administrator-only
pgokf_private.sync_logrecords one row per successfulregister/refresh/register_bundle_contentsync and perunregister, inside the operation's own transaction (so a logged row always means the operation committed). Read it with the reader-levelpgokf.list_sync_log(bundle_id, max_rows)(returning the newpgokf.sync_log_entry). This also activates the previously deadsync_log_retention_dayskey: after each append, history older than the window is pruned in the same transaction (0keeps it indefinitely). - Bundle enable/disable lifecycle.
pgokf.set_bundle_enabled(bundle_id, enabled)(writer-tier) hides a bundle from ranked search and graph traversal without deleting any rows, and is fully reversible. concept_neighborsnow excludes disabled bundles, matchingconcept_search, so a disabled bundle's concepts are neither returned nor traversed.- Change notification. A new
notify_channelconfiguration key: when set to a safe channel identifier, a successful sync emitspg_notify(<channel>, {bundle_id, op, added, updated, removed, total}). Off by default (empty) with zero overhead. - Observability functions (all reader-level):
pgokf.catalog_stats()(per-bundle indexed-concept / link / resolved-link counts, sync recency, and a 24-hour staleness flag →pgokf.catalog_stat),pgokf.health()(ajsonbliveness/readiness document:ok, counts,search_backend,bm25_ready,in_recovery,roles_ok,config_ok), andpgokf.stale_concepts(bundle_id, as_of)(concepts past their OKFstale_after→pgokf.stale_concept). - OKF version conformance. A new
okf_version_policykey (warn|reject, defaultwarn): a bundle declaring an OKFokf_versionthis build does not support (only0.2/0.2.x) is warned about and indexed underwarn, or rejected with22023underreject. An absentokf_versionis unaffected. Theokf-parsercrate gains a small, centralizedis_supported_okf_version.
Changed¶
sync_log_retention_daysmoves from defined-but-dead to active (see above).notify_channelandokf_version_policyare new, active keys.- Internal: a behavior-preserving complexity refactor of the parser,
config-coercion, and SPI-row-reading hot paths - a shared
spi_readtuple helper (DRY), per-key config coercion/defaults, and decomposed ISO-8601 parsers - dropping the worst function's cyclomatic complexity from 39 to 18 with no change to any behavior, signature, SQL surface, or test.
0.1.4 - 2026-08-28¶
Two additive capabilities landed together: a pgokf_writer ingestion role
tier paired with an optional BM25 search backend, and a mountless
object-store ingestion path (register_bundle_content plus the standalone
pgokf-ingest companion). Everything here is backward compatible - new
functions, a new role, a new projection column, and a new configuration key -
so ALTER EXTENSION pgokf UPDATE TO '0.1.4' migrates an existing install in a
single transaction (the 0.1.3 → 0.1.4 upgrade script creates the writer role,
adds rebuild_search_index, and adds register_bundle_content +
bundles.source_type).
Added¶
pgokf_writerrole - a new ingestion tier betweenpgokf_readerandpgokf_admin(pgokf_reader<pgokf_writer<pgokf_admin, each inheriting the tier below). It is the intended account for an automated ingestion pipeline: it can register/refresh/unregister bundles but cannot change configuration, write exports, or readpgokf_private.pgokf.register_bundle_content(name text, paths text[], contents bytea[], options jsonb)- the mountless ingestion path. A companion process reads an object store and
streams the collected
(path, bytes)into PostgreSQL; the extension itself performs no network or filesystem I/O. Re-calling it resyncs the bundle (changed concepts upserted, missing ones deleted) exactly like a filesystem refresh, with the samemax_bundle_files/max_file_bytesbounds andstore_sourceround-trip. Writer-tier,SECURITY DEFINER. pgokf.bundles.source_type('filesystem'|'content', default'filesystem') distinguishing a bundle registered from a canonical on-disk root from one streamed in memory (keyed on the synthetic pathcontent:<name>).pgokf.rebuild_search_index()- admin function that (re)builds the optionalpg_searchBM25 index; a no-op with a notice whenpg_searchis not installed.search_backendconfiguration key (native|bm25, defaultnative).nativeuses the built-inwebsearch_to_tsquery/ts_rank_cdranking;bm25routesconcept_searchthrough ParadeDBpg_searchat runtime via SPI when available, falling back to native (with a warning) when it is not - so the extension takes no hard dependency onpg_search.pgokf-ingestcompanion crate - a standalone async binary that lists an S3-compatible object store (MinIO / SeaweedFS / AWS S3 / GCS / Azure viaobject_store), downloads the objects, and streams them toregister_bundle_contentaspgokf_writer. Object-store credentials live in the companion and never reach PostgreSQL. It is a separate workspace member and does not affect the extension build.
Changed¶
- Ingestion moved to the writer tier (backward compatible).
pgokf.register_bundle,pgokf.refresh_bundle, andpgokf.unregister_bundlenow requirepgokf_writerinstead ofpgokf_admin. Existing admin callers keep working becausepgokf_admininheritspgokf_writer; configuration and the file-writing exports remain admin-only. refresh_bundlerejects content-sourced bundles. Asource_type = 'content'bundle has no filesystem root, sorefresh_bundleraises22023for it - re-sync those by callingregister_bundle_contentagain.- Internal: the sync engine was refactored around a
ByteSourceseam so the filesystem path (walk + read) and the content path (caller-supplied bytes) share one classify → parse → upsert → project pipeline. Filesystemregister_bundle/refresh_bundlebehavior is unchanged.
0.1.3 - 2026-08-28¶
OKF v0.2 conformance re-model of the provenance / trust / lifecycle projection,
and population of pgokf.bundles.okf_version. This is a breaking change to
the pgokf.concept_provenance shape; because the extension is pre-release (no
tagged release, no external installs), the schema is changed in place with no
compatibility shim.
Changed¶
pgokf.concept_provenancere-modeled to OKF v0.2 (breaking). The invented, non-OKF columnsverified(a flattened bool),verification_method, andfreshnessare removed. The table now carries the real OKF v0.2 fields:generated_by(generated.by),generated_at(generated.at),status(LIFECYCLEstatus),stale_after,usage_window_from/usage_window_to(top-levelusage_window), and atrust_tierderived from the verification actors (unverified→machine-confirmed→human-reviewed). Timestamps are ISO 8601, parsed defensively (a malformed instant projectsNULL, never aborting the sync); the recognized key subset is kept losslessly indetails. The index onverifiedis replaced by an index ontrust_tier.pgokf.bundles.okf_versionis now populated. The sync engine reads the optionalokf_versionfrom the reserved bundle-rootindex.mdfrontmatter (string or number, e.g.0.2) and stores it; an absent or malformed value leaves the columnNULL. It surfaces unchanged throughbundle_info/list_bundles.
Added¶
pgokf.concept_verificationtable - the ordered OKFverified[]event list, one(bundle_id, concept_id, ordinal)row per{by, at}event (a singleverifiedmapping is stored as oneordinal = 0row; actorless events are skipped). Cascades frompgokf.concepts; reader-SELECTable.pgokf.concept_provenance_sourcetable - the OKFsources[]provenance materials, one row per entry (source_id,resource,title,author,usage_count,last_modified, per-sourceusage_window_from/_to). Distinct from the raw-bytespgokf.concept_source. Cascades frompgokf.concepts; reader-SELECTable.
Fixed¶
export_parquetepoch cast for OKF v0.2 provenance timestamps. The re-modeledpgokf.concept_provenance.generated_atis atimestamptz;export_parquetnow converts it to epoch microseconds ((EXTRACT(EPOCH FROM generated_at) * 1000000)::bigint) so the Parquet writer emits a portableTimestamp(µs, UTC)column. Verified round-trippable in DuckDB via an in-database test.
Security¶
- Closed an
export_sourceswrite-escape via a symlinked parent directory.export_sourcesrecreates a bundle's directory tree underdest_dir; a symlink planted at an intermediate path component could previously redirect a write outside the validated destination. Writes now use the sameO_NOFOLLOWopen asexport_parqueton the final component and re-validate every stored concept path as a plain bundle-relative path, so a planted symlink is refused (22023) instead of followed. Each reconstructed file is additionally verified against its recorded BLAKE3file_hashbefore creation (XX000on mismatch, nothing written).
Upgrade¶
- No supported in-place upgrade from
0.1.2: this pre-release drops and re-creates the provenance projection. Re-CREATE EXTENSIONand re-register bundles; because the on-disk bundle is the source of truth, the projection is fully rebuilt from a sync.
0.1.2 - 2026-08-27¶
Additive, opt-in raw source storage. Default behavior is unchanged: the new
store_source policy is off by default, so an install that never enables it
is byte-for-byte identical to 0.1.1.
Added¶
store_sourceconfiguration key (boolean, defaultfalse) onpgokf_private.config. It selects between two deployment tiers:truestores each concept's verbatim source bytes in PostgreSQL (small, self-contained install - no external storage needed);falsekeeps the source in a mounted object store / data lake and PostgreSQL holds only metadata and search. Likedefault_text_search_config, it is read at sync time and is not retroactive - set it before the firstregister_bundle, or re-register.pgokf.concept_sourcetable - opt-in verbatim source bytes (raw_content bytea,byte_size integer), keyed(bundle_id, concept_id)and cascading frompgokf.concepts, so removals and unregistration drop the stored source automatically. TOAST-compressed withlz4where the build supports it, otherwisepglz. Reader-SELECTable.pgokf.get_concept_source(bundle_id, concept_id) → bytea- reader-level retrieval of a concept's exact stored bytes to the client (no filesystem write). Raises22023when the concept exists but no source was stored, and, distinctly, when no such concept exists.pgokf.export_sources(bundle_id, dest_dir) → pgokf.export_result- admin-only reconstruction of a bundle's stored source files on disk, byte-for-byte. Reusesexport_parquet's destination validation andO_NOFOLLOWfile creation, recreates the bundle-relative directory tree, and verifies each written file against the concept's BLAKE3file_hash.
Changed¶
- The sync engine now persists source bytes into
pgokf.concept_sourcewhenstore_sourceis enabled, projected inside the same atomic, advisory-locked transaction as links and provenance (no change when the key is off).
Upgrade¶
ALTER EXTENSION pgokf UPDATE TO '0.1.2'brings a 0.1.1 install fully to 0.1.2 with no data loss: it adds thestore_sourcecolumn (defaultfalse), theconcept_sourcetable, and the two new functions, and touches no existing object.
0.1.1 - 2026-08-27¶
Hardening, performance, and packaging. No public-API change: the stable surface
(functions, types, tables, roles, GUCs) is byte-for-byte identical to 0.1.0, so
ALTER EXTENSION pgokf UPDATE TO '0.1.1' is a proven no-data-loss step.
Fixed¶
- A large concept body could abort an otherwise-valid sync. The body
tsvectoris now fully bounded so no document within the configured size limits can raise PostgreSQL'stsvectorsize error mid-sync; the whole transaction no longer rolls back on a single large-but-in-limit file. - Resolved the findings from a full-repository adversarial audit across the parser, sync engine, and catalog surface - input-validation edges, error mapping, and path-handling corners hardened without changing behavior for well-formed input.
Performance¶
- Batched SPI inserts in the sync engine - concepts, metadata, links, and provenance are projected in batched statements instead of row-at-a-time, cutting per-file round trips on large bundles.
- Guarded the link re-resolution
UPDATEso an incrementalrefresh_bundleonly re-resolves links whose target set actually changed, avoiding needless writes on unchanged concepts.
Added¶
- Distribution packaging -
.deb/.rpmbuild recipes, a PGXNMETA.json, a Docker image, and a Homebrew formula, wired into apackagesCI job so per-major artifacts build reproducibly. - Proven extension upgrade path. The example
sql/pgokf--0.1.0--0.1.1.sqlupgrade script exercisesALTER EXTENSION pgokf UPDATE TO '0.1.1'end to end as a deliberate no-op, demonstrating the forward-compatible, never-DROP/TRUNCATE/DELETEmigration contract thattests/api_stability.rsenforces on every shipped script.
0.1.0 - 2026-08-27¶
The first tagged release: a complete, transactional PostgreSQL catalog for Open Knowledge Format (OKF) bundles. The bundle on disk stays the portable source of truth; PostgreSQL becomes a projection optimized for metadata queries, native full-text search, and link-graph traversal.
Added¶
- Bundle registration and sync.
pgokf.register_bundle(path, name, options)ingests an OKF bundle root andpgokf.refresh_bundle(bundle_id)incrementally re-synchronizes it, re-parsing only files whose BLAKE3 content hash changed and removing rows for deleted files.pgokf.unregister_bundle(bundle_id)removes a bundle; concepts, metadata, links, and provenance cascade. - Catalog projection. Base tables
pgokf.bundles,pgokf.concepts, andpgokf.concept_metadata, plus the feature projectionspgokf.links(concept-to-concept link graph) andpgokf.concept_provenance(generation and verification lineage). - Full-text search.
pgokf.concept_search(query, bundle_id, limit)returns ranked hits withts_headlinesnippets over a weightedtsvector(title A, tags/type/description B, body D). Native PostgreSQL FTS only - no third-party search extension is required. - Link-graph traversal.
pgokf.concept_neighbors(concept_id, max_hops, bundle_id)walks the resolved link graph outward from a concept. - Administration.
pgokf.list_bundles()andpgokf.bundle_info(bundle_id)expose the registered-bundle inventory as thepgokf.bundle_infotype. - Durable configuration.
pgokf.set_config,pgokf.reset_config, andpgokf.get_configmanage a single, typed, cluster-persistent policy row (allowed_roots,default_text_search_config,default_strict,sync_log_retention_days,default_exclude) stored in the administrator-onlypgokf_private.configtable. - Parquet export.
pgokf.export_parquet(bundle_id, dest_dir)writes a bundle's catalog projection to four Parquet files -concepts.parquet,concept_metadata.parquet,links.parquet, andconcept_provenance.parquet - inside
dest_dirfor downstream analytics. - Version introspection.
pgokf.version()reports the loaded shared library's version for post-upgrade agreement checks. - Composite result types.
pgokf.bundle_sync_result,pgokf.concept_search_result,pgokf.concept_neighbor,pgokf.bundle_info, andpgokf.export_result. - Security model. Two cluster roles,
pgokf_reader(search and read configuration) andpgokf_admin(register/refresh/unregister and manage configuration, inheritspgokf_reader). Every mutating function isSECURITY DEFINERwith a pinnedsearch_path,EXECUTEis revoked fromPUBLICand granted only to the appropriate role, and bundle paths are validated (absolute, traversal-free, canonicalized, optionally confined to configuredallowed_roots) before the server reads any file. The privatepgokf_privateschema is internal state, not API. - Configurable safety limits (GUCs).
pgokf.max_file_bytes,pgokf.max_bundle_files,pgokf.max_frontmatter_bytes,pgokf.max_graph_hops, andpgokf.log_level. - Documentation coverage. Every public object - all 12 functions, all 5
composite types, all 6 catalog tables, and both API roles - carries a
COMMENT ON, enforced by theapi_stabilitytest suite and by a runtimeobj_descriptioncoverage gate in the release checklist. - PostgreSQL 15–19 support, built with Rust (edition 2024) and pgrx 0.19.
- Extension upgrade mechanism. A documented, forward-compatible example
upgrade path (
pgokf--0.1.0--0.1.1.sql) exercisesALTER EXTENSION pgokf UPDATEwith a proven no-data-loss guarantee.
Security¶
- Path traversal, symlink escape, NUL-byte, and relative-path inputs to
register_bundleare rejected before any filesystem access. - The
pgokf_privateschema and itsconfigtable are readable and writable only by the extension owner andpgokf_admin; readers cannot see policy.