Adapted from docs/admin/PageOwnership.md in the Wikantik repository; current as of release 2.4.56.
Every wiki page tracked by the structural index has a single designated owner — the
login_name of the user responsible for it. Ownership is stored in the page_owners
table (V034) and is independent of the page ACL: it records accountability, not
read/write permission.
The page_owners table schema (added in V034):
CREATE TABLE IF NOT EXISTS page_owners (
canonical_id TEXT PRIMARY KEY,
owner_login TEXT, -- NULL = orphaned
assigned_by TEXT NOT NULL,
assigned_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Key invariants:
canonical_id, not on the mutable page slug. A page rename does not
change ownership.owner_login IS NULL means orphaned. The admin UI surfaces these in the
"Orphaned" tab. Deleting a user account (/admin/users, single or bulk) orphans
every page that user owned, stamping assigned_by with
system:user-deleted:<login>.PageOwnerService.getOwner() resolves an owner, it
applies an admin fallback: if the stored owner_login is NULL, blank, or no
longer a real user, the returned value is admin. This means callers always get a
non-null string; the raw NULL is only visible through the admin REST endpoints.agents service accountPages authored by AI agents typically carry a author: frontmatter field that is not
a real login name. Rather than leaving those pages orphaned, PageOwnerService
bootstraps them onto a dedicated service account.
V035 seeds this account:
| Field | Value |
|---|---|
login_name | agents |
wiki_name | Agents |
full_name | AI Agents (service account) |
email | agents@localhost |
| Roles | none (non-loginable) |
Because agent-authored pages default to this owner, the Orphaned tab is empty by
design on a well-configured deployment — pages do not become ownerless at save time;
they belong to agents. Pages become orphaned only when their owner's account is
deleted or an admin orphans them on purpose.
The property that names this default:
wikantik.page_ownership.default_owner = agents
If the agents account does not exist in the database (e.g. after a hard DB reset
without re-running V035), PageOwnerService falls back safely to NULL (orphaned)
rather than persisting a dangling owner_login.
The one-time backfill of pre-existing pages onto agents is not part of V035 (no
data fixups in versioned migrations). If you need it, run:
psql -h localhost -U wikantik -d wikantik \
-f bin/db/one-shots/backfill-agent-default-owner.sql
PageOwnershipSaveFilter runs as a post-save PageFilter. After every successful
page save it calls PageOwnerService.getOwner(canonicalId), which is a find-or-create:
page_owners row already exists for the canonical_id, it is returned as-is.author frontmatter field and
checks whether that value is a real login_name.
assigned_by = system:bootstrap.default_owner
(agents) is used, provided it is itself a real user.owner_login = NULL (orphaned).This filter is gated by:
wikantik.page_ownership.enforcement.enabled = true
Setting this to false disables bootstrap; no page_owners rows are written on save.
Admins can reassign ownership at any time via the admin UI or REST API (see below).
Reassignment stamps assigned_by with the login of the acting admin and updates
assigned_at.
| Property | Default | Effect |
|---|---|---|
wikantik.page_ownership.enforcement.enabled | true | Enables post-save bootstrap via PageOwnershipSaveFilter. |
wikantik.page_ownership.default_owner | agents | Login used when the frontmatter author cannot be resolved to a real user. Must be a real user in the database to take effect. |
Properties go in wikantik-custom.properties (in tomcat/tomcat-11/lib/).
Route: /admin/page-ownership
The page has two tabs and a bulk form at the top.
Located above the tabs. Transfers all pages from one owner to another in a single request.
<orphaned> (to adopt all orphaned
pages).<orphaned> (to orphan all pages currently owned by
"From owner").Lists pages where owner_login IS NULL. Columns: Page name, Canonical ID, Owner,
Assigned by, Assigned at, Actions.
An empty Orphaned tab is the expected steady state when the agents account is
properly seeded.
Enter a login (or <orphaned>) in the search box and press Search. Results show
every page owned by that login.
Click Reassign on any row to open an inline modal. Enter the new owner login (or
<orphaned> to orphan the page) and confirm.
All endpoints are under /admin/page-ownership/* and are pre-authorized by
AdminAuthFilter (requires AllPermission — admin role). CORS is disabled on this
resource.
GET /admin/page-ownershipQuery parameters:
| Parameter | Required | Values | Notes |
|---|---|---|---|
filter | yes | orphaned or by-owner | orphaned lists pages with owner_login IS NULL. |
owner | when filter=by-owner | login or <orphaned> | <orphaned> is equivalent to filter=orphaned. |
limit | no | integer, 1–500 | Default 50. |
offset | no | integer ≥ 0 | Default 0. |
Response:
{
"pages": [
{
"canonicalId": "01KQEDYJR57WYQCV645PKSDBMQ",
"pageName": "KgInclusionPolicy",
"ownerLogin": "jsmith",
"assignedBy": "system:bootstrap",
"assignedAt": "2026-04-27T10:00:00Z"
}
],
"total": 1
}
pageName resolves the canonical_id through the structural index at response time; it
is null when the structural index is unavailable or has no entry for that id.
POST /admin/page-ownership/reassignPer-page reassignment. Validates that newOwner exists before writing.
Request body:
{
"pages": ["01KQEDYJR57WYQCV645PKSDBMQ", "01KQABC..."],
"newOwner": "jsmith"
}
Use "newOwner": "<orphaned>" to set owner_login = NULL.
Response: { "updated": 2 }
POST /admin/page-ownership/reassign-by-userBulk transfer — moves every page from one owner to another in a single SQL UPDATE.
Validates toOwner exists (unless it is <orphaned>).
Request body:
{
"fromOwner": "jsmith",
"toOwner": "agarcia"
}
Sentinel values:
fromOwner | toOwner | Effect |
|---|---|---|
<orphaned> | a login | Adopt all currently-orphaned pages. |
| a login | <orphaned> | Orphan all pages owned by that login. |
<orphaned> | <orphaned> | No-op (returns { "updated": 0 }). |
Response: { "updated": 47 }
All /admin/page-ownership/* endpoints are protected by AdminAuthFilter, which
requires the AllPermission security permission — effectively the admin role. There
is no finer-grained access: any admin can reassign any page.
The acting admin's login is recorded in assigned_by for every write.
Ownership is an accountability record, not an access-control gate. It does not grant or restrict read, write, or delete on the page. Permission is still governed by:
[{ALLOW view Admin}]() syntax in page content).policy_grants (managed at /admin/security).groups + group_members.Ownership informs the comments/mentions system: the page owner is included in
is_owner_mention rows when someone comments on a page (see comment_mentions table,
also added in V034). See docs/user/CommentsAndMentions.md for details.
Orphaned tab is unexpectedly full
Either the owner accounts were deleted (look at assigned_by: a value of
system:user-deleted:<login> names the deleted account; reassign those pages from the
Orphaned tab), or the agents service account was not seeded (V035 may not have run,
or the DB was reset). For the second case, check:
psql -h localhost -U wikantik -d wikantik -c "SELECT login_name FROM users WHERE login_name = 'agents';"
If the account is absent, re-run the migration:
bin/db/migrate.sh # idempotent — V035 uses ON CONFLICT DO NOTHING
Owner shown as admin but the page has a different author
PageOwnerService.getOwner() applies an admin fallback when the stored
owner_login is NULL or the user no longer exists. Use the By Owner tab to
search for <orphaned> or the former login, then reassign.
pageName is null in the API response
The structural index is unavailable or has not indexed the page yet. The page is
still reachable by canonicalId. Start Tomcat and wait for the index to populate;
custom-jdbc provider lag can be up to ~20 seconds after startup.
Enforcement filter not running
Check that wikantik.page_ownership.enforcement.enabled is not false in
wikantik-custom.properties, and that the structural index (Page Graph
subsystem) initialized successfully — the filter resolves a page's
canonical_id via StructuralIndexService::resolveCanonicalIdFromSlug, and a
failure there is caught and logged at WARN rather than blocking the save.
Look for PageOwnershipSaveFilter: enforcement enabled in catalina.out.
docs/user/CommentsAndMentions.md — the comment_mentions table
(also added in V034) uses is_owner_mention to notify page owners.bin/db/migrations/V034__page_owners_and_mentions.sql — DDL.bin/db/migrations/V035__seed_agents_service_account.sql — agents account seed.Cluster Hub: Wikantik Platform Development Hub: Architecture, MCP Protocols, and Agent Briefings (wikantik-development)