Deep dive

Wiki Primitive

Plain-markdown wiki pages on disk. Open them in Obsidian. Sign them with ed25519. Scope reads to entities. The wiki is your durable scratchpad — and the agents' too.

Updated May 2, 2026


The Ouroboros wiki is markdown files on disk. Not a database with an export button — actual .md files in a folder you can open, grep, version-control, and edit in Obsidian without the daemon running. Wikilinks, embeds, tags, and frontmatter all work the way you’d expect, because they are the way you’d expect.

What Ouroboros adds on top is a SQLite index (so search is fast), an ed25519 signature on every write (so tampering is detectable), and a scope clause on every read (so a connection bound to one entity can’t see notes for another). Files are the source of truth. The DB is a projection. If the row goes away, the file is still on disk and re-indexes on the next scan.

Files first, DB second

Pages live at ~/Ouroboros/vault/wiki/<kind>/<slug>.md. That folder is yours. Open it in Obsidian, point a vault at it, set up your graph view — the wikilinks ([[other-page]]), the tag pills, the embeds, the YAML frontmatter — they’re all standard Obsidian-flavored markdown.

The daemon watches the folder. When a file changes — whether the agent wrote it, you edited it in Obsidian, or you mv’d it from the terminal — the daemon re-parses the frontmatter and updates the SQLite index row. The index carries the title, kind, entity scope, tags, link graph, and the ed25519 signature. The index is disposable. The file is not.

Kinds and slugs

Every page has a kind and a slug. The kind determines which folder the file lives in and which index columns get populated. A few common ones:

  • note — freeform; the default
  • arc-scratchpad — the agent’s working notes for a multi-step arc
  • entity-profile — durable summary of one entity
  • decision — an architectural call with the rationale
  • glossary — a defined term

The slug is the filename. Kebab-case, no extension in the call. So { kind: 'decision', slug: 'switch-to-libsql' } becomes ~/Ouroboros/vault/wiki/decision/switch-to-libsql.md.

Frontmatter shape

Every page starts with YAML frontmatter. The minimum is title and kind; everything else is optional but useful:

---
title: "Switch to libSQL"
kind: decision
entity_id: ouroboros-app
tags: [storage, migration]
links:
- sqlite-to-libsql-tradeoffs
- 2026-q2-roadmap
created_at: 2026-04-12T14:03:00Z
updated_at: 2026-05-02T09:11:00Z
---

# Switch to libSQL

We're moving the daemon's local store from raw SQLite to libSQL because...

The index reads frontmatter. If you set entity_id, the page is scoped to that entity. If you don’t, it’s a global note for your user. Tags become queryable. The links array is your explicit forward-reference list — the inline [[wikilinks]] are picked up too, but the explicit list is what gets canonicalized in the graph.

Scope-aware reads

A connection scoped to one entity cannot read pages tagged to another entity. The daemon injects the scope clause at the SQL layer — the same wikiScopeClause() helper used for every other entity-bound table. The rule is straightforward:

  • If the page has no entity_id, it’s global to the user — visible everywhere.
  • If the page has an entity_id, it’s visible only to connections whose scope includes that entity.
  • If the page is tagged to the special __inbox__ scope, it’s visible to all connections regardless of scope (used for inter-agent posts).

A scoped agent that calls list_wiki_pages simply doesn’t see the rows it isn’t allowed to. There’s no “permission denied” error to leak the existence of a page; the row just isn’t in the result set. The clause is enforced in SQL, not at the application layer.

ed25519 signatures

Every wiki write is signed with the daemon’s session-bound ed25519 key. The signature lands in the index row next to the file path and a hash of the content. You can verify a page hasn’t been edited out from under the daemon — or that the index hasn’t been swapped — via sophia.lint_wiki_page.

This is a tamper-evident layer, not encryption. The markdown stays plain text on disk so Obsidian and your text editor still work. What the signature buys you: if someone (or some buggy script) overwrites the file outside the daemon flow, the next lint catches the signature mismatch and surfaces it as a warning. You decide what to do — accept the new content and re-sign, or restore from the daemon’s mutation journal.

The agent uses it too

The wiki isn’t just a place for you to take notes. When an agent claims a multi-day arc, the convention is that it maintains a scratchpad page at wiki/arc-scratchpad/<arc-slug>.md — progress notes, blocked-on items, open questions, decisions taken. Other agents read that file before starting their own work on the same arc. Cross-session continuity, no special tooling.

Wiki pages also surface in sophia.search_documents, scope-permitted, so an agent searching for “the lease amendment we discussed last week” gets hits across the wiki and the ingested document corpus in one query. Your notes and the source documents live in the same retrieval layer.

Architecture

agent.write_wiki_page → file written → signed → indexed → scope-aware reads
flowchart LR
  A[Agent or you] -->|sophia.write_wiki_page| D[Daemon]
  D -->|fs.write| F[~/Ouroboros/vault/wiki/kind/slug.md]
  D -->|ed25519 sign| S[(signature)]
  D -->|UPSERT| I[(SQLite wiki index)]
  F -.watch.-> D
  O[Obsidian] -.reads.-> F
  R[read_wiki_page] -->|scope clause| I
  R -->|fs.read| F
  R --> A2[Agent or you]

Sample API surface

Writing a page from an agent:

await sophia.write_wiki_page({
kind: 'decision',
slug: 'switch-to-libsql',
title: 'Switch to libSQL',
body_md: '# Switch to libSQL\n\nWe are moving the daemon...',
frontmatter: {
  entity_id: 'ouroboros-app',
  tags: ['storage', 'migration'],
  links: ['sqlite-to-libsql-tradeoffs'],
},
});

Reading one back:

const page = await sophia.read_wiki_page({
kind: 'decision',
slug: 'switch-to-libsql',
});
// → {
//     path: '~/Ouroboros/vault/wiki/decision/switch-to-libsql.md',
//     title: 'Switch to libSQL',
//     kind: 'decision',
//     entity_id: 'ouroboros-app',
//     tags: ['storage', 'migration'],
//     links: ['sqlite-to-libsql-tradeoffs'],
//     body_md: '# Switch to libSQL\n\n...',
//     signature: 'ed25519:9f3a...',
//     signature_valid: true,
//     created_at: '2026-04-12T14:03:00Z',
//     updated_at: '2026-05-02T09:11:00Z',
//   }

The other tools round out the surface:

  • sophia.list_wiki_pages({ kind?, entity_id?, tags? }) — index lookup with scope automatically applied
  • sophia.list_pages_by_tag({ tag }) — tag index, also scope-clipped
  • sophia.lint_wiki_page({ kind, slug }) — verify the signature, validate the link graph, surface broken [[wikilinks]]
  • sophia.update_wiki_page({...}) — append-or-replace edit; every update is mutation-journaled and revertable through Time Machine

Where this is headed

  • Bidirectional graph in the dashboard — today the link graph is computed but only surfaced to agents. The tray dashboard will render it as an interactive map of your knowledge, the same shape Obsidian’s graph view shows you, but scope-aware.
  • Agent-proposed edits as PRs — instead of agents writing directly, an agent can propose an edit (a diff against an existing page) that you approve from the tray. Approved edits are journaled with the proposer’s attested identity, so the audit trail tells you which agent suggested what.
  • Inline-citation surfaces — every quoted fact in a wiki page links back to the source document with a line range. Hover a quote, see the provenance chain. Agents writing wiki notes are already expected to cite; the surface makes it browsable.

← Back to overview