Concepts
The words in the app are Space, Folder and Page. The code and the API say space, cluster and node. They are the same things.
Workspaces, Spaces, Folders and Pages
- A workspace is one git repository at
/data/ws/<workspace>/repoplus its own search index. Every sign-up gets its own workspace, and members are invited into it. Data never crosses workspaces; an id from another workspace returns 404. - A Space is a top-level folder (
work/,clients/). Each Space has anindex.mdthat acts as its catalogue and heads bundles scoped to it. - A Folder (cluster) is any folder inside a Space, nestable (
clients/dlp/). A Folder'sindex.mdis its summary, andlog.mdis an append-only log written throughcontext_logorPOST /api/v1/log. - A Page (node) is one
.mdfile. It has a stableid(a ULID in frontmatter, added by the server if missing), so links and history survive renames.
---
id: 01J9X...
title: DLP overview
type: project # note|project|person|company|decision|log|index|skill|thread|capture
sensitivity: internal # public | internal | confidential | secret (default internal)
tags: [client, dlp]
summary: One line used by bundles and index.md
status: active # free text; "closed" makes it an archive candidate
pinned: false # pinned pages are never auto-archived
---
# DLP
Text anyone with internal access can read. See [[Pricing]].
[[Wiki links]] and relative .md links become graph edges, which context_related and bundles follow.
Sensitivity
Four levels, lowest to highest: public < internal < confidential < secret. A level applies to a whole Page through frontmatter, or to a block inside it:
<!-- confidential -->
Contract value and terms.
<!-- /confidential -->
<!-- secret -->
Stored sealed in git; readable only by an owner after step-up.
<!-- /secret -->
- Everyone has a ceiling. Content above it is replaced by
[REDACTED: <level>, ask owner], never summarised. A Page whose own level is above your ceiling is simply not found. - Role ceilings: owner
secret, editorconfidential, viewerinternal. - Secret blocks and the bodies of
sensitivity: secretPages are sealed with AES-GCM before they reach git, so a clone only showsnxc-sealed:v1:ciphertext. Secret text is never full-text indexed or embedded. Titles and frontmatter are not sealed. - Raising sensitivity is always allowed. Lowering it needs a human with step-up; for an agent it becomes a proposal, and over git it is always rejected.
Principals: humans and agents
A principal is anyone who acts: a human member or an agent. Every read and write is attributed to one.
- Humans have a workspace role:
owner,editor(shown as member) orviewer(shown as guest). They sign in with email, password and optional TOTP, and hold a web session. - Agents are created by a human on the Agents page. Each agent has its own keys (
nxc_), its own grants, an optional ceiling, and a creator. An agent can never do more than its creator's role currently allows: demote the creator and the agent shrinks on its next call. - OAuth connectors (Claude.ai, ChatGPT) become agents too, created at the consent screen with the Spaces and ceiling you choose there.
Grants
A grant is {space, cluster_glob, actions[], max_sensitivity}, shown in the app as space/glob : actions : level.
| Action | Lets the agent |
|---|---|
read | Get pages, bundles, tree, related |
search | Search |
propose | Queue changes for review, append to logs |
write | Commit directly, archive and restore |
collab | Post on threads and mention others |
watch | Reserved for subscriptions (planned) |
admin | Create Spaces, delete pages, push over git (owners grant it) |
assign, capture | Reserved for tasks and quick capture (planned) |
The default grant for a new agent is * / ** : read, search, propose, collab, watch : internal. Standing agent grants stop at confidential whatever you ask for; secret is only reachable through a time-boxed elevation approved by an owner with step-up. Viewers can only grant read, search, propose, collab, watch, and nobody can grant above their own ceiling.
Proposals
Agents propose, humans decide. A proposal is a full new version of a Page plus a reason, waiting for an editor or owner on the Proposals screen (word diff, accept, edit and accept, reject).
Proposals are created when:
- an agent calls
context_proposeorPOST /api/v1/proposals; - an agent without
writetries to archive (context_archive); - a write targets an owner-only path (
index.md,CLAUDE.md,AGENTS.md,.gitattributes, anything under askills/folder); - a write would lower sensitivity or move a page to a wider audience;
- two writes collide and cannot be merged (the losing write is kept as a proposal and the call returns 409).
Writes and conflicts
Every write goes through a single writer queue per workspace. Send the hash you read as If-Match (REST) or base_sha (MCP). If someone changed the Page since, a clean three-way merge lands (merged: true); a real conflict returns 409 with a proposal_id. Nothing is silently overwritten when a base is given. The web editor always sends it.
Threads and the inbox
Any Page can carry a thread. Messages are stored in core.db and also appended to <page>.threads.md beside the Page, so the discussion is in git. Mentions (@name or a principal id) reach only principals who can read the Page, and land in their inbox together with replies, proposals and conflicts.
Agent traffic is guarded: 20 messages per minute and 300 per day per principal, at most 5 mentions per message, no @all or @here except from owners, duplicate bodies within 10 minutes are refused. After 4 agent-to-agent hops the thread is flagged for a human, and 20 agent messages in 10 minutes lock it until an editor unlocks it.
Bundles
context_bundle(intent) is the tool agents should call first. It runs hybrid keyword and semantic search, filters by your grants before ranking, adds the relevant Folder index.md, deduplicates, packs the best sections into a token budget (default 4000) and cites the source path and commit for every section. Each section is fenced with a per-response nonce so stored text cannot pose as instructions. Only Pages actually emitted in a bundle count as reads.
Lifecycle and archive
Every Page has last_touched_at, the later of its last read and last edit. A nightly job at 03:00 UTC finds archive candidates:
- untouched for longer than the Folder's
auto_archive_days(120 by default); status: closedin frontmatter;superseded_byset.
Closing a whole Folder is a separate, manual action (see below).
Pinned pages and type: index or type: skill are exempt. With LIFECYCLE_MODE=dry (the default) the job only lists candidates on the Archive screen. With apply it archives them.
Archiving sets state: archived in frontmatter and commits; files never move. Archived Pages drop out of bundles, search and tree unless include_archived is set, while context_get by id still works. Archive a whole Folder to close a matter; restore brings it back.
Audit
Every read, write, denial, token and grant change is recorded in an append-only, hash-chained audit log (ids only, never content). Owners see it on the Audit screen, and npm run verify-audit checks the chain.