@promptowl/contextnest-community 1.23.0 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/API.md +2092 -0
  2. package/CONFIGURATION.md +1 -0
  3. package/README.md +61 -9
  4. package/STEWARDSHIP.md +229 -0
  5. package/dist/{chunk-2YPY2HGZ.js → chunk-2TPQTN4Y.js} +5 -4
  6. package/dist/{chunk-MLQU4I5J.js → chunk-5EZOPA47.js} +5 -5
  7. package/dist/{chunk-UOMW7TT6.js → chunk-I3CSD6CK.js} +1 -1
  8. package/dist/{chunk-QLXC6542.js → chunk-KIAAEHWL.js} +49 -25
  9. package/dist/{chunk-QWNXWRWO.js → chunk-LA3VTQ22.js} +25 -1
  10. package/dist/{chunk-XYD6V2LH.js → chunk-XUIWAWDO.js} +28 -33
  11. package/dist/{chunk-5CVZMHHB.js → chunk-ZTT4U4NE.js} +2 -2
  12. package/dist/{client-LWDMNKX3.js → client-KEY4PYJH.js} +1 -1
  13. package/dist/{engine-QFF2IA3G.js → engine-S3QBQ7LH.js} +2 -2
  14. package/dist/{external-edit-service-F7D3VSA2.js → external-edit-service-6FNFPOJJ.js} +3 -3
  15. package/dist/{grants-service-TBGTCO5K.js → grants-service-UT3EQ3R7.js} +2 -2
  16. package/dist/index.js +371 -90
  17. package/dist/{migrations.postgres-APCVSYUE.js → migrations.postgres-AIQ7WSU7.js} +45 -4
  18. package/dist/{review-service-A7PNLWG5.js → review-service-HHGOTO6P.js} +6 -6
  19. package/dist/{stewardship-service-32UHIRQ5.js → stewardship-service-4DHNRULG.js} +3 -3
  20. package/dist/{version-service-RJ6CWZ3O.js → version-service-KIOQME6U.js} +3 -3
  21. package/dist/web3/assets/ActivityTracePage-BOZHgJ8S.js +1 -0
  22. package/dist/web3/assets/AgentDocsPage-CIiaBqMy.js +1 -0
  23. package/dist/web3/assets/CollaboratorManager-By91wrIr.js +1 -0
  24. package/dist/web3/assets/CollaboratorsTab-D1EE64Fs.js +1 -0
  25. package/dist/web3/assets/DocumentEditor-DrQfrW3d.js +36 -0
  26. package/dist/web3/assets/DocumentsTab-Vba-AzBv.js +6 -0
  27. package/dist/web3/assets/ExternalEditsTab-BSzAQIGM.js +1 -0
  28. package/dist/web3/assets/MarkdownEditor-74N_naNQ.css +1 -0
  29. package/dist/web3/assets/MarkdownEditor-C9zqZjC5.js +643 -0
  30. package/dist/web3/assets/NestPageHeader-CoLUV1Oz.js +1 -0
  31. package/dist/web3/assets/NestView-CTZ55tXD.js +63 -0
  32. package/dist/web3/assets/OverviewTab-Dxi-Hu-N.js +1 -0
  33. package/dist/web3/assets/PersonCombobox-CPOlNs6G.js +1 -0
  34. package/dist/web3/assets/ReasonDialog-DgLxfRWv.js +1 -0
  35. package/dist/web3/assets/ReviewActions-OfOdqzCn.js +6 -0
  36. package/dist/web3/assets/ReviewTab-CyX8yxd3.js +1 -0
  37. package/dist/web3/assets/StewardsTab-DBEEfAQI.js +1 -0
  38. package/dist/web3/assets/SubmitForReviewModal-Ci2DdWs0.js +1 -0
  39. package/dist/web3/assets/alert-dialog-DTozKlmV.js +7 -0
  40. package/dist/web3/assets/arrow-left-BCKt4CwQ.js +6 -0
  41. package/dist/web3/assets/backlinks-CYd4xENL.js +24 -0
  42. package/dist/web3/assets/card-XG2dP5Xf.js +1 -0
  43. package/dist/web3/assets/chevron-left-BPwUT9DS.js +6 -0
  44. package/dist/web3/assets/circle-check-DI8IBGeo.js +6 -0
  45. package/dist/web3/assets/circle-x-VKVhC88W.js +6 -0
  46. package/dist/web3/assets/code-xml-CMabYZvH.js +6 -0
  47. package/dist/web3/assets/corner-down-right-BGxwK6wH.js +6 -0
  48. package/dist/web3/assets/count-skeleton-DMbdpscX.js +1 -0
  49. package/dist/web3/assets/dates-BCxbm4_q.js +1 -0
  50. package/dist/web3/assets/earth-BEp1EKTo.js +6 -0
  51. package/dist/web3/assets/file-exclamation-point-r8qZGIjP.js +6 -0
  52. package/dist/web3/assets/folder-input-Cmtx1Jhk.js +11 -0
  53. package/dist/web3/assets/folder-target-CUSWqImF.js +1 -0
  54. package/dist/web3/assets/index-BM-h3DwI.css +1 -0
  55. package/dist/web3/assets/index-C1wTSjfP.js +29 -0
  56. package/dist/web3/assets/index-EaX2yql0.js +389 -0
  57. package/dist/web3/assets/page-B3yvQvHy.js +1 -0
  58. package/dist/web3/assets/page-BExMvttJ.js +1 -0
  59. package/dist/web3/assets/page-BUOADv2X.js +1 -0
  60. package/dist/web3/assets/page-BYUFhYWZ.js +1 -0
  61. package/dist/web3/assets/page-Bicf02Cz.js +1 -0
  62. package/dist/web3/assets/page-BsPMDm5d.js +45 -0
  63. package/dist/web3/assets/page-ByyVLDVo.js +16 -0
  64. package/dist/web3/assets/page-CC0NJi8v.js +1 -0
  65. package/dist/web3/assets/page-CHb0F5hV.js +24 -0
  66. package/dist/web3/assets/page-CPjWQNKx.js +11 -0
  67. package/dist/web3/assets/page-CQt3ZGbf.js +2 -0
  68. package/dist/web3/assets/page-Cj1NetQ-.js +1 -0
  69. package/dist/web3/assets/page-CtaW65El.js +1 -0
  70. package/dist/web3/assets/page-DGOL9l8i.js +6 -0
  71. package/dist/web3/assets/page-title-ClvvBsIo.js +1 -0
  72. package/dist/web3/assets/play-C5YNKpAH.js +6 -0
  73. package/dist/web3/assets/refresh-cw-BhHzSH9m.js +6 -0
  74. package/dist/web3/assets/scroll-area-dRWncRqa.css +1 -0
  75. package/dist/web3/assets/scroll-area-hz-xtayP.js +1 -0
  76. package/dist/web3/assets/select-DBFTrUmI.js +6 -0
  77. package/dist/web3/assets/send-0CHvmluT.js +6 -0
  78. package/dist/web3/assets/settings-CeeF4LEb.js +6 -0
  79. package/dist/web3/assets/share-2-CMnOlOt-.js +6 -0
  80. package/dist/web3/assets/tag-DQ_6J5Gv.js +11 -0
  81. package/dist/web3/assets/trash-2-HBi-Pslz.js +6 -0
  82. package/dist/web3/assets/triangle-alert-CDy8-7sv.js +6 -0
  83. package/dist/web3/assets/user-plus-CvBNcpn4.js +6 -0
  84. package/dist/web3/assets/x-By7piikG.js +6 -0
  85. package/dist/web3/assets/zap-C2T8riBp.js +11 -0
  86. package/dist/web3/index.html +2 -2
  87. package/package.json +4 -2
  88. package/dist/web3/assets/index-BJ-LRNis.js +0 -1382
  89. package/dist/web3/assets/index-DPMEt-A_.css +0 -1
package/CONFIGURATION.md CHANGED
@@ -80,6 +80,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
80
80
  | `POSTHOG_KEY` | `""` | PostHog project API key for product analytics in the UI. Empty = analytics off (the default — a self-hosted install brings its own project). Served to the browser via `/health`, but only while `TELEMETRY_ENABLED` is on, so that switch turns off everything this server sends outward. Also editable from Settings → Advanced. |
81
81
  | `POSTHOG_HOST` | `https://us.i.posthog.com` | PostHog ingestion host. Set it to your own region or self-hosted PostHog. Also editable from Settings → Advanced. |
82
82
  | `TRACE_RETENTION_DAYS` | `14` | Activity-trace retention window in days (the `api_events` rows behind `GET /admin/trace` and `GET /nests/:id/trace`). Rows older than this are pruned opportunistically (every ~500 inserts). `0` = keep forever (pruning is skipped entirely). Capped at `3650`; invalid/negative values fall back to `14`. Also editable from Settings → Advanced. |
83
+ | `DRIFT_SCAN_INTERVAL_MS` | `30000` (30 s) | How often the drift scanner walks every nest for files edited outside the app (external edits) and stages them as suggestions for review. Set `0` to disable — do this on a GCS FUSE or other network mount, where the walk is slow and costly. Unset, empty or non-numeric falls back to the default. |
83
84
  | `CORS_ORIGINS` | `*` in open mode; `http://localhost:5173,http://localhost:3838` in key mode | Comma-separated allowlist. Set to `*` to allow any origin (**only** safe in open mode — in key mode with Bearer tokens this enables CSRF). |
84
85
  | `FRAME_ANCESTORS` | `'self'` | Which origins may embed this server in an iframe, sent as CSP `frame-ancestors`. The default lets nothing but this origin frame the UI, which blocks clickjacking. Deployments that are meant to be embedded list the embedding origin — e.g. the PromptOwl Data Room iframes ContextNest, so that install sets `FRAME_ANCESTORS="https://app.promptowl.ai"`. Comma-separated; `'self'` is always included; `*` allows any site and disables the protection. Note the embedding page must be **same-site** (a sibling subdomain) for the session cookie to survive inside the frame — a genuinely cross-domain embed will render the login page no matter what this is set to. |
85
86
  | `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. The asset-upload route (`POST /nests/:id/assets`) is exempt up to the video cap below. |
package/README.md CHANGED
@@ -8,17 +8,19 @@
8
8
 
9
9
  ## What it is
10
10
 
11
- ContextNest Community Edition is a self-hosted server that lets you:
11
+ A shared knowledge base for your team **and** your AI agents, running on your own machine or infrastructure. Documents live in **nests**; every save is a version; in a stewarded nest a change waits for a reviewer before agents can read it. People write in the browser, agents read and write the same documents over MCP, HTTP or the `ctx` CLI.
12
12
 
13
- - Store, version, and govern markdown-based context documents ("nests"), filed in folders you can reorganize as the vault grows
14
- - Import an existing folder or vault of markdown files in one step
15
- - Export a nest as a portable bundle and re-import it on another self-hosted host
16
- - Apply stewardship workflows — draft, pending review, approved
17
- - Share nests with collaborators or publish them read-only to the public
18
- - Serve approved context to AI agents via MCP or HTTP — connect one by pasting a generated setup prompt or downloading a ready `.env`, or register the nest in the ctx CLI (`ctx vault add <alias> --url <server>/nests/<id>/mcp --bearer-env CONTEXTNEST_API_KEY`, then `ctx query "#tag" --vault <alias>`)
19
- - Sync with the PromptOwl hosted platform for multi-user collaboration
13
+ Concretely, it lets you:
20
14
 
21
- The server runs locally or on your own infrastructure. Your PromptOwl account handles authentication, entitlement, and governance metadata.
15
+ - Write Markdown documents — plus hosted HTML artifacts and CSV tables agents can query row by row — filed in folders, linked with `[[Title]]`, tagged, commented on, and versioned
16
+ - Import an existing folder of Markdown in one step; export a nest as a portable bundle
17
+ - Govern changes — draft → pending review → approved — with stewards per nest, tag or document, and a cross-nest inbox (My Work, My Drafts) for what is waiting on you
18
+ - Share a nest with people or teams, share a single document or folder, or publish read-only to the public
19
+ - Give agents deterministic reads: the same selector returns the same context every time, with a trace of what was pulled
20
+ - Run agents against a nest (Workflows, Beta): an agent is a document whose body is instructions; its output lands as a pending review
21
+ - Sync with the PromptOwl hosted platform for accounts, licensing and teams
22
+
23
+ Your PromptOwl account handles authentication, entitlement, and governance metadata.
22
24
 
23
25
  ## Quickstart
24
26
 
@@ -73,6 +75,40 @@ The key is read at boot. The server validates against PromptOwl on startup; if v
73
75
 
74
76
  For redistribution, hosted-service, OEM, or regulated-industry licensing, contact **hoot@promptowl.ai**.
75
77
 
78
+ ## First five minutes
79
+
80
+ 1. **Create a nest** on the Nests page — or **Import folder** to bring in Markdown you already have.
81
+ 2. **Write a document.** `[[Title]]` links another document, `@name` mentions a person, tags go under the title. Every save is a version.
82
+ 3. **Turn on stewardship** (nest Settings) if you want review. Saves become drafts; **Submit for review**; a steward approves or rejects. Alone? Allow self-approve and you get the history without the ceremony.
83
+ 4. **Connect an agent.** Inside a nest, **Connect** (bottom of the sidebar) gives you the MCP URL and a REST snippet; **Add with AI** (nest overview) is a prompt that has an agent write context in. Mint a key under Workspace → API keys.
84
+ 5. **Share.** The whole nest with a person or team, one document or folder, or make it public with a read-only Reader mode.
85
+
86
+ The in-app **How it works** page (sidebar → Help) is the full tour; **Docs** (header) is the agent manual.
87
+
88
+ ## Around the app
89
+
90
+ One sidebar, grouped:
91
+
92
+ | Group | Pages | Who |
93
+ |---|---|---|
94
+ | — | **Nests** — every nest you can see, pinned first | everyone |
95
+ | Work | **My Work** (reviews and handoffs waiting on you, across nests) · **My Drafts** (yours, never submitted) | everyone |
96
+ | Workspace | **Teams** · **API keys** | everyone |
97
+ | Admin | **Teammates** · **Server settings** · **Activity trace** | server admins |
98
+ | Help | **How it works** | everyone |
99
+
100
+ Inside a nest: the document tree, plus **Board** (documents as cards by folder, status or tag), **Graph** (documents, links, tags, stewards), **Definitions** (the nest's glossary) and **Workflows** (Beta). The user menu (top right) holds **Account** (name, password), keyboard shortcuts and logout. `Ctrl`/`⌘` `K` searches every nest and document; `?` lists shortcuts.
101
+
102
+ ## Connect an agent
103
+
104
+ Point any agent at `<server>/llms.txt` — it is the complete manual (endpoints, MCP tools, selector grammar). The short version:
105
+
106
+ - **MCP** — `<server>/mcp` for everything the key can read, `<server>/nests/<id>/mcp` for one nest with the full toolset. Claude Code, Cursor and VS Code connect over HTTP with `Authorization: Bearer cnst_…`; Claude Desktop goes through `npx -y mcp-remote`.
107
+ - **REST** — `POST /nests/<id>/context` with a selector (`#tag`, `[[Title]]`, `type:document`, combined with `+ | -`) returns assembled context plus a trace. Full reference: [API.md](./API.md).
108
+ - **ctx CLI** — `ctx vault add <alias> --url <server>/nests/<id>/mcp --bearer-env CONTEXTNEST_API_KEY`, then `ctx query "#tag" --vault <alias>`.
109
+
110
+ Keys are minted in the app (Workspace → API keys, or the Connect dialog) — one per client, user-wide or scoped to a nest. Servers in `AUTH_MODE=open` need none.
111
+
76
112
  ## System requirements
77
113
 
78
114
  - **Node.js** 20.x or later
@@ -105,6 +141,14 @@ For redistribution, hosted-service, OEM, or regulated-industry licensing, contac
105
141
  | MCP server for AI agents | ✅ | ✅ |
106
142
  | One-shot agent setup — generated connect prompt + `.env` download | ✅ | ✅ |
107
143
  | Several API keys per account — one credential per client, rotate one at a time | ✅ | ✅ |
144
+ | Comments on documents — anchored threads, resolve / reopen, same threads over MCP | ✅ | ✅ |
145
+ | Table nodes — CSV facts with deterministic row queries (`POST /table-query`, `context_table_query`) | ✅ | ✅ |
146
+ | Teams — share a nest with a group once; import rosters from PromptOwl | ✅ | ✅ |
147
+ | Cross-nest inbox — My Work (reviews, handoffs) and My Drafts | ✅ | ✅ |
148
+ | Nest views — Board (kanban by folder / status / tag), Graph, Definitions glossary | ✅ | ✅ |
149
+ | Workflow plane (Beta) — agents as documents, runs, schedules, inbound hooks, Slack / Teams / webhook connectors | ✅ | ✅ |
150
+ | Activity trace — every governance action, per nest and server-wide | ✅ | ✅ |
151
+ | Notifications — Slack, Microsoft Teams, email | ✅ | ✅ |
108
152
  | Centralized multi-tenant admin console | — | ✅ |
109
153
  | Single sign-on (OIDC — Entra ID, Google, Okta, Keycloak) | ✅ | ✅ |
110
154
  | SAML / SCIM provisioning | — | ✅ |
@@ -118,6 +162,14 @@ For Enterprise pricing and features, contact **hoot@promptowl.ai** or visit <htt
118
162
 
119
163
  Release notes live in [CHANGELOG.md](./CHANGELOG.md).
120
164
 
165
+ ## Documentation
166
+
167
+ - [CONFIGURATION.md](./CONFIGURATION.md) — every environment variable (port, auth mode, storage, notifications, telemetry)
168
+ - [API.md](./API.md) — complete REST reference with request and response bodies
169
+ - [STEWARDSHIP.md](./STEWARDSHIP.md) — the governance model: modes, stewards, scopes, prime documents, teams, super-admins
170
+ - `<server>/llms.txt` — the agent manual the running server publishes; rendered as **Docs** in the app
171
+ - **How it works** inside the app — a page-by-page tour
172
+
121
173
  ## Licensing
122
174
 
123
175
  ContextNest Community Edition is **commercial software**. It is **not open source**.
package/STEWARDSHIP.md ADDED
@@ -0,0 +1,229 @@
1
+ # Stewardship — Quick Guide
2
+
3
+ A short, practical guide to the governance model the community server implements. The deep strategy lives in `docs/contextnest-whitepaper.md`; this is the "how does this actually work in the product" version.
4
+
5
+ ## The two modes
6
+
7
+ Every nest is either **ungoverned** or **governed**, controlled by the `stewardship_enabled` flag on the nest.
8
+
9
+ | | Ungoverned (default) | Governed |
10
+ |---|---|---|
11
+ | New documents | Auto-approved, immediately available to AI | Start as **drafts**; need steward approval |
12
+ | Draft / pending / rejected lifecycle | Not shown | Shown, filterable |
13
+ | Review queue | Hidden | Visible, orderable |
14
+ | Stewards | Not shown | Assignable by scope |
15
+ | Reads gated by permission | No | Yes (in key mode) |
16
+
17
+ Flip the flag any time via the nest's **Settings → Stewardship**. Existing approved docs stay approved; existing drafts remain drafts.
18
+
19
+ ## What a steward is
20
+
21
+ A **steward** is a person (identified by email) who governs a subset of the nest. Stewards have one of three roles:
22
+
23
+ | Role | Can read | Can edit | Can approve / reject |
24
+ |---|---|---|---|
25
+ | **Viewer** | yes | no | no |
26
+ | **Editor** | yes | yes | no |
27
+ | **Reviewer** | yes | yes | **yes** |
28
+
29
+ > **Authors can't approve their own work.** Even a reviewer-level steward can't approve a version they themselves authored. Separation of duties, enforced server-side.
30
+
31
+ ## Scope — who governs what
32
+
33
+ Stewards are assigned at one of three scopes. When a document needs a steward (to approve, or to check access), the server resolves them in this order — **first match wins**:
34
+
35
+ | Priority | Scope | Target | Example |
36
+ |---|---|---|---|
37
+ | 1 | **Document** | Exact node id | `nodes/pricing-policy` — only this one doc |
38
+ | 2 | **Tag** | Tag name (lowercased, no `#`) | `security` — any doc tagged `#security` |
39
+ | 3 | **Nest** | (none — applies to the whole nest) | The fallback for anything not covered above |
40
+
41
+ If nothing matches and no steward resolves, the **nest owner** is the implicit steward (owner fallback). That keeps a freshly-enabled nest workable even with zero stewards configured.
42
+
43
+ ## The approval flow
44
+
45
+ 1. **Author** creates or edits a doc. In governed mode it saves as a **draft**.
46
+ 2. **Author** clicks **Submit for Review**. The doc becomes **pending** and appears in the review queue for anyone whose scope resolves to that doc — and in their **My Work** page across nests. Until someone acts the author can withdraw it back to draft.
47
+ 3. A **Reviewer steward** opens the review queue (or My Work), reads the doc, clicks **Approve** or **Reject**. Rejecting requires a note.
48
+ 4. On approve, the doc becomes **approved** and is now AI-readable. The approved version number is pinned (`approvedVersion`), so subsequent drafts don't automatically replace it — AI keeps getting the last blessed version until a new one is approved.
49
+
50
+ ### Auto-publish owner & admin edits (`allow_self_approve`)
51
+
52
+ A per-nest flag (**off** by default; nest Settings → "Auto-publish owner & admin edits"). When it's **on**, a create or edit by the nest **owner** or an **admin** publishes immediately — it skips steps 2–4 entirely instead of landing as a draft. This is a governance *bypass* for the two roles that already hold approval authority, not a separation-of-duties exception. **Editor and reviewer flows are unchanged**: their edits still save as drafts and go through Submit → Review → Approve regardless of the flag. The pending-review edit lock still applies, so an owner/admin can't overwrite a doc a teammate has in review. Turning the flag back off doesn't unpublish anything; it only affects future edits.
53
+
54
+ ## Prime documents
55
+
56
+ Most content in a nest doesn't need a review gate — call notes, social drafts, scratch research. A small set does: messaging architecture, the sales playbook, pricing policy. **Prime** marks that second set.
57
+
58
+ A prime document **always** goes through draft → pending → approved, no matter who writes it. It is the one thing `allow_self_approve` does not bypass: an owner editing a prime document still lands a draft.
59
+
60
+ ### How a document becomes prime
61
+
62
+ Resolution mirrors the steward scope order — **document → tag → nest**, first match wins:
63
+
64
+ | Priority | Scope | How it's set |
65
+ |---|---|---|
66
+ | 1 | **Document** | The Prime switch in the document editor (owner/admin only). `true` forces prime; `false` **exempts** this one doc from an otherwise-prime tag. |
67
+ | 2 | **Tag** | The doc carries one of the nest's **prime tags** (Settings → Prime tags; `prime-document` out of the box). |
68
+ | 3 | **Nest** | Nothing matched → not prime. |
69
+
70
+ Only the nest owner or an admin can set or clear the document flag — it's a governance decision, not an authoring one. Otherwise any editor could un-flag the playbook and walk their own edit past the gate.
71
+
72
+ ### Reviewing prime documents only (`prime_only_review`)
73
+
74
+ A per-nest flag (**off** by default; nest Settings → "Review prime documents only"). It changes what a governed nest's *default* is:
75
+
76
+ | | `prime_only_review` off (default) | on |
77
+ |---|---|---|
78
+ | Ordinary document, any author | Draft → review → approve (unless the author has a self-approve bypass) | **Publishes immediately** |
79
+ | Prime document, any author | Draft → review → approve | Draft → review → approve |
80
+
81
+ That's the "most content self-publishes, high-stakes content is gated" posture. Leaving it off preserves the original behaviour, so turning stewardship on doesn't silently change meaning for an existing nest.
82
+
83
+ While `prime_only_review` is on, `allow_self_approve` has no effect at all — non-prime documents already publish for everyone, and prime documents ignore the bypass. Settings disables that switch and says so rather than leaving a live control that changes nothing.
84
+
85
+ Prime only applies to **governed** nests. With stewardship off there are no stewards to approve anything, so a prime tag is inert until you turn stewardship on.
86
+
87
+ ### At a glance
88
+
89
+ Prime documents carry a **Prime** badge in the nest's document list, with a tooltip saying whether it came from the document flag or an inherited tag. To see every document a given prime tag covers, filter the list by that tag.
90
+
91
+ Flipping any of this only affects **future** writes. Marking a published document prime doesn't unpublish it; the next edit is what needs approval.
92
+
93
+ ## Who sees what
94
+
95
+ Governance gates two things: who can **approve**, and who can **read**.
96
+
97
+ - **Approve gate**: server-side, always on when stewardship is enabled. Non-stewards can't approve.
98
+ - **Read gate**: only active in **key mode** (`AUTH_MODE=key`, real user accounts with API keys). In **open mode** — which is the default for a solo/local server — everyone is the same anonymous admin, so read-gating is a no-op.
99
+
100
+ In key mode, when stewardship is enabled on a nest:
101
+ - Node list / single-read / search / query responses are filtered to docs the caller can access
102
+ - A non-steward gets an empty list or a 403 on the node they asked for
103
+ - The nest **owner** and any resolved **steward (any role)** can read; **super admins** bypass the check
104
+
105
+ ## Server super-admins
106
+
107
+ Super-admins administer **every** nest on the server without being added per-nest. The effective set is the union of three sources:
108
+
109
+ - **license** — the PromptOwl account that owns the installed license (always; never revocable)
110
+ - **config** — emails in `access.yaml: super_admins` (the bootstrap; edit the file + restart to change)
111
+ - **granted** — grants managed from **Teammates → Superadmins** in the UI, or `GET/POST /admin/super-admins` (see `API.md`)
112
+
113
+ A super-admin can:
114
+
115
+ - Read every document (bypasses the read gate, even with stewardship enabled)
116
+ - Change a nest's visibility (private / org / public)
117
+ - Add, remove, and re-role collaborators
118
+ - Add, remove, and re-role stewards
119
+ - Approve and reject pending versions in any nest's review queue
120
+ - Manage the official community sites trusted for SSO auto-login (**Server settings → Community sites**, or `GET/POST /admin/community-sites` — see `API.md`)
121
+
122
+ What a super-admin **cannot** do (owner-only operations):
123
+
124
+ - Delete a nest
125
+ - Transfer ownership
126
+
127
+ Use this for a small set of platform operators — a CEO, a head of governance, an internal compliance admin — not as a default broad-access mechanism. Day-to-day governance should still flow through per-nest stewards.
128
+
129
+ ## Teams — sharing with a group
130
+
131
+ Instead of adding people one at a time, you can create a **team** — a reusable,
132
+ owner-managed group — and share it onto a nest. Each team member carries a
133
+ **role** set on the membership (not on the share), and that role applies on
134
+ **every** nest the team is shared to:
135
+
136
+ | Member role | Nest access | Governance |
137
+ |---|---|---|
138
+ | **viewer** | read | viewer |
139
+ | **editor** | write | editor |
140
+ | **admin** | admin | admin |
141
+
142
+ Key points:
143
+
144
+ - A member's role is uniform across nests — set it once on the team. To vary
145
+ access per nest, use a different team (or a direct collaborator/steward grant).
146
+ - When a user reaches a nest by several paths (a direct collaborator grant **and**
147
+ team membership), the **highest** access wins.
148
+ - Sharing a team whose members carry a governance role (viewer/editor)
149
+ **enables stewardship** on the nest — same effect as assigning an individual
150
+ steward. An `admin`-only team is collaborator-style and doesn't.
151
+ - Removing a member, deleting the team, or unsharing it **revokes access
152
+ immediately** — there are no per-user rows to clean up.
153
+ - Managing a team is gated by your role on it: editor/admin members (and the
154
+ owner) can add people, capped to their own level; admins can also change
155
+ roles and remove members; only the owner can delete the team.
156
+
157
+ ### Importing a team from PromptOwl
158
+
159
+ If you already run teams in PromptOwl, the Teams page has a **PromptOwl Teams**
160
+ tab listing your teams there with their members. Each one has a **Sync** button
161
+ that copies it here as a normal team owned by you, instead of you retyping every
162
+ member. Members without an account here become invited placeholders, exactly as
163
+ if you'd added them by hand.
164
+
165
+ **The tab only appears if you signed in with PromptOwl** — that sign-in is what
166
+ gives this server permission to read your teams. Signing in another way (SSO,
167
+ your identity provider, or email and password) means no PromptOwl identity to
168
+ read teams for, so the tab isn't offered.
169
+
170
+ **You can import a team you're an owner or editor of in PromptOwl.** Teams you're
171
+ view-only on are listed for reference but can't be imported: the copy would be
172
+ yours to control here — rename it, rewrite its roster, share it onto a nest —
173
+ and that's more than PromptOwl lets you do with that team. Ask one of its owners
174
+ or editors to import it instead.
175
+
176
+ Already-imported teams are marked **Synced** in that tab, and show a **PromptOwl**
177
+ badge in the ContextNest Teams tab. Two things worth knowing before you sync
178
+ one again:
179
+
180
+ - **PromptOwl is the source of truth on every sync.** The roster is reconciled to
181
+ match: roles are reset, and anyone no longer in the PromptOwl team is removed —
182
+ including people you added to this team by hand here. If you need a group that
183
+ differs from PromptOwl, make it a separate team rather than editing an
184
+ imported one.
185
+ - **Your PromptOwl access isn't stored on this server.** The connection lives in
186
+ a cookie in your own browser and ends when your session does — this server's
187
+ database never holds anything that can reach your PromptOwl account, so a
188
+ stolen backup of it exposes nothing of yours. The connection is also limited
189
+ to reading your profile and your teams: it cannot chat as you or spend your
190
+ credits. Signing out ends it, and revoking the device in PromptOwl's account
191
+ settings ends it immediately from the other side.
192
+
193
+ PromptOwl's `User` role — its default membership — maps to **viewer** here, as
194
+ does any role this version doesn't recognize; `Owner` maps to admin, and
195
+ `Editor`/`Viewer` map across as themselves.
196
+
197
+ Members see the teams they belong to on the **Teams** page and any team-shared
198
+ nest on their dashboard. Manage teams there (create, add/remove members with a
199
+ role); share them from a nest's sharing panel. See `API.md` §3a for the endpoints.
200
+
201
+ ## Picking a scope when you assign a steward
202
+
203
+ Rules of thumb:
204
+
205
+ - **Nest scope** — use sparingly. "This person reviews everything in this nest."
206
+ - **Tag scope** — the most common. "Legal reviews anything with `#legal`. Security reviews `#auth` and `#pii`."
207
+ - **Document scope** — override. "This specific doc has a dedicated reviewer regardless of its tags."
208
+
209
+ You can stack them: a doc tagged `#legal` can have a doc-level steward that overrides the tag steward. Priority order resolves ties.
210
+
211
+ ## `stewards.yaml` (optional)
212
+
213
+ You can also declare stewards declaratively via a `stewards.yaml` file in the nest's vault directory and run `POST /nests/:id/stewards/sync`. Useful for version-controlled governance. Example:
214
+
215
+ ```yaml
216
+ version: 1
217
+ nest:
218
+ - email: governance-lead@acme.com
219
+ role: reviewer
220
+ tags:
221
+ "#legal":
222
+ - email: legal@acme.com
223
+ role: reviewer
224
+ "#security":
225
+ - email: security-team@acme.com
226
+ role: reviewer
227
+ ```
228
+
229
+ Sync replaces all stewards for that nest from the file. Prefer the UI for day-to-day adds; prefer the file for reproducible setups and code review.
@@ -8,10 +8,11 @@ import {
8
8
  nestAllowsSelfApprove,
9
9
  nestName,
10
10
  primaryRole,
11
+ resolveNestAccess,
11
12
  resolveNestPermission,
12
13
  resolveTeamRolesForUser,
13
14
  sendEmailToRecipient
14
- } from "./chunk-QLXC6542.js";
15
+ } from "./chunk-KIAAEHWL.js";
15
16
  import {
16
17
  ConflictError,
17
18
  ValidationError
@@ -20,7 +21,7 @@ import {
20
21
  config,
21
22
  getDb,
22
23
  isEmailish
23
- } from "./chunk-QWNXWRWO.js";
24
+ } from "./chunk-LA3VTQ22.js";
24
25
 
25
26
  // src/governance/stewardship-service.ts
26
27
  import { v4 as uuid } from "uuid";
@@ -441,8 +442,8 @@ async function resolveUserRoles(nestId, userEmail, opts) {
441
442
  }
442
443
  async function canManageStewards(nestId, userId) {
443
444
  if (config.AUTH_MODE === "open") return true;
444
- const perm = await resolveNestPermission(nestId, userId);
445
- return perm === "owner" || perm === "admin";
445
+ const { rawPermission } = await resolveNestAccess(nestId, userId);
446
+ return rawPermission === "owner" || rawPermission === "admin";
446
447
  }
447
448
  async function canCreateInNest(nestId, userEmail) {
448
449
  if (config.AUTH_MODE === "open" || isSuperAdmin(userEmail)) return true;
@@ -4,19 +4,19 @@ import {
4
4
  resolveNestWideRoles,
5
5
  resolveStewardsForNode,
6
6
  stewardCoverageForUser
7
- } from "./chunk-2YPY2HGZ.js";
7
+ } from "./chunk-2TPQTN4Y.js";
8
8
  import {
9
9
  grantCoversNode,
10
10
  listUserGrants,
11
11
  resolveNodeGrant
12
- } from "./chunk-UOMW7TT6.js";
12
+ } from "./chunk-I3CSD6CK.js";
13
13
  import {
14
14
  createVersion,
15
15
  getApprovedVersion,
16
16
  getApprovedVersions,
17
17
  getCurrentVersion,
18
18
  setApprovedVersion
19
- } from "./chunk-5CVZMHHB.js";
19
+ } from "./chunk-ZTT4U4NE.js";
20
20
  import {
21
21
  buildDocContext,
22
22
  buildTitleMap,
@@ -37,7 +37,7 @@ import {
37
37
  resolveNestPermission,
38
38
  sendEmailToRecipient,
39
39
  titleForNode
40
- } from "./chunk-QLXC6542.js";
40
+ } from "./chunk-KIAAEHWL.js";
41
41
  import {
42
42
  ConflictError,
43
43
  NotFoundError,
@@ -46,7 +46,7 @@ import {
46
46
  import {
47
47
  config,
48
48
  getDb
49
- } from "./chunk-QWNXWRWO.js";
49
+ } from "./chunk-LA3VTQ22.js";
50
50
 
51
51
  // src/governance/review-service.ts
52
52
  import { v4 as uuid2 } from "uuid";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-YVMSM7LS.js";
4
4
  import {
5
5
  getDb
6
- } from "./chunk-QWNXWRWO.js";
6
+ } from "./chunk-LA3VTQ22.js";
7
7
 
8
8
  // src/governance/grants-service.ts
9
9
  import { v4 as uuid } from "uuid";
@@ -8,7 +8,7 @@ import {
8
8
  config,
9
9
  getDb,
10
10
  isEmailish
11
- } from "./chunk-QWNXWRWO.js";
11
+ } from "./chunk-LA3VTQ22.js";
12
12
  import {
13
13
  ANON_USER_ID
14
14
  } from "./chunk-YB3LKF7U.js";
@@ -1296,8 +1296,11 @@ async function listNotifications(userEmail, opts = {}) {
1296
1296
  CASE WHEN dr.target_type = 'document' THEN dr.node_id END,
1297
1297
  -- A mention names the document directly; there's no request row.
1298
1298
  CASE WHEN n.kind = 'mention' THEN n.subject_id END
1299
- ) AS node_id
1299
+ ) AS node_id,
1300
+ -- Name resolved here so the inbox never fetches the nest list for it.
1301
+ COALESCE(ns.name, n.nest_id) AS nest_name
1300
1302
  FROM notifications n
1303
+ LEFT JOIN nests ns ON ns.id = n.nest_id
1301
1304
  LEFT JOIN review_requests rr ON rr.id = n.subject_id
1302
1305
  LEFT JOIN deletion_requests dr ON dr.id = n.subject_id
1303
1306
  WHERE LOWER(n.user_email) = LOWER(?)${opts.unreadOnly ? " AND n.read_at IS NULL" : ""}
@@ -2488,6 +2491,9 @@ var FlatNestStorage = class extends NestStorage2 {
2488
2491
  }
2489
2492
  };
2490
2493
 
2494
+ // src/nodes/node-index.ts
2495
+ import { listSuggestions } from "@promptowl/contextnest-engine";
2496
+
2491
2497
  // src/shared/node-id.ts
2492
2498
  function assertSafeNodeId(rawId) {
2493
2499
  let id = rawId;
@@ -2616,13 +2622,21 @@ async function ensureNodeIndex(nestId) {
2616
2622
  return run;
2617
2623
  }
2618
2624
  async function rebuildNodeIndex(nestId) {
2619
- const { engineCache: engineCache2 } = await import("./engine-QFF2IA3G.js");
2620
- const { documentsWithSuggestions } = await import("./external-edit-service-F7D3VSA2.js");
2625
+ const { engineCache: engineCache2 } = await import("./engine-S3QBQ7LH.js");
2626
+ const { documentsWithSuggestions } = await import("./external-edit-service-6FNFPOJJ.js");
2621
2627
  const { storage, dropDiscoveryCache } = await engineCache2.get(nestId);
2622
2628
  dropDiscoveryCache();
2623
2629
  const startedAt = writeGeneration.get(nestId) ?? 0;
2624
2630
  const documents = await storage.discoverDocuments({ includeRetired: true });
2625
2631
  const staged = await documentsWithSuggestions(nestId);
2632
+ const stagedMetas = (await Promise.all(
2633
+ [...staged].map(
2634
+ (id) => listSuggestions(storage, id).catch((err) => {
2635
+ console.error("[node-index] listSuggestions failed", nestId, id, err);
2636
+ return [];
2637
+ })
2638
+ )
2639
+ )).flat();
2626
2640
  const db = getDb();
2627
2641
  const insertSql = insertOrReplace(
2628
2642
  db,
@@ -2630,18 +2644,15 @@ async function rebuildNodeIndex(nestId) {
2630
2644
  ["nest_id", "node_id"],
2631
2645
  UPSERT_SET
2632
2646
  );
2633
- const stagedSql = insertOrIgnore(
2634
- db,
2635
- "INSERT INTO node_suggestions (nest_id, node_id) VALUES (?, ?)"
2636
- );
2647
+ const stagedSql = insertOrIgnore(db, SUGGESTION_INSERT);
2637
2648
  await db.transaction(async (tx) => {
2638
2649
  await tx.run("DELETE FROM node_index WHERE nest_id = ?", [nestId]);
2639
2650
  for (const doc of documents) {
2640
2651
  await tx.run(insertSql, [nestId, ...rowValues(doc)]);
2641
2652
  }
2642
2653
  await tx.run("DELETE FROM node_suggestions WHERE nest_id = ?", [nestId]);
2643
- for (const id of staged) {
2644
- await tx.run(stagedSql, [nestId, id]);
2654
+ for (const m of stagedMetas) {
2655
+ await tx.run(stagedSql, suggestionValues(nestId, m));
2645
2656
  }
2646
2657
  if ((writeGeneration.get(nestId) ?? 0) !== startedAt) return;
2647
2658
  await tx.run(
@@ -2651,30 +2662,42 @@ async function rebuildNodeIndex(nestId) {
2651
2662
  });
2652
2663
  return documents.length;
2653
2664
  }
2654
- async function setSuggestionFlag(nestId, nodeId, present) {
2665
+ var SUGGESTION_INSERT = "INSERT INTO node_suggestions (nest_id, node_id, suggestion_id, source, actor, detected_at, target_hash, proposed_hash, note) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
2666
+ var suggestionValues = (nestId, m) => [
2667
+ nestId,
2668
+ m.document_id,
2669
+ m.suggestion_id,
2670
+ m.source,
2671
+ m.actor,
2672
+ m.detected_at,
2673
+ m.target_hash,
2674
+ m.proposed_hash,
2675
+ m.note ?? null
2676
+ ];
2677
+ async function syncSuggestionRows(nestId, nodeId, metas) {
2655
2678
  try {
2656
2679
  const db = getDb();
2657
- if (!present) {
2658
- await db.run(
2680
+ const insertSql = insertOrIgnore(db, SUGGESTION_INSERT);
2681
+ await db.transaction(async (tx) => {
2682
+ await tx.run(
2659
2683
  "DELETE FROM node_suggestions WHERE nest_id = ? AND node_id = ?",
2660
2684
  [nestId, nodeId]
2661
2685
  );
2662
- return;
2663
- }
2664
- await db.run(
2665
- insertOrIgnore(
2666
- db,
2667
- "INSERT INTO node_suggestions (nest_id, node_id) VALUES (?, ?)"
2668
- ),
2669
- [nestId, nodeId]
2670
- );
2686
+ for (const m of metas) await tx.run(insertSql, suggestionValues(nestId, m));
2687
+ });
2671
2688
  } catch (err) {
2672
- console.error("[node-index] suggestion flag failed", nestId, nodeId, err);
2689
+ console.error("[node-index] suggestion rows failed", nestId, nodeId, err);
2673
2690
  }
2674
2691
  }
2692
+ async function listSuggestionRows(nestId) {
2693
+ return await getDb().all(
2694
+ "SELECT * FROM node_suggestions WHERE nest_id = ? ORDER BY detected_at DESC",
2695
+ [nestId]
2696
+ );
2697
+ }
2675
2698
  async function nodesWithSuggestions(nestId) {
2676
2699
  const rows = await getDb().all(
2677
- "SELECT node_id FROM node_suggestions WHERE nest_id = ?",
2700
+ "SELECT DISTINCT node_id FROM node_suggestions WHERE nest_id = ?",
2678
2701
  [nestId]
2679
2702
  );
2680
2703
  return new Set(rows.map((r) => r.node_id));
@@ -2916,7 +2939,8 @@ export {
2916
2939
  markIndexStale,
2917
2940
  ensureNodeIndex,
2918
2941
  rebuildNodeIndex,
2919
- setSuggestionFlag,
2942
+ syncSuggestionRows,
2943
+ listSuggestionRows,
2920
2944
  nodesWithSuggestions,
2921
2945
  indexedNodeIds,
2922
2946
  readIndexedNodes,
@@ -1757,6 +1757,30 @@ function runMigrations(db) {
1757
1757
  recordMigration("042_community_sites");
1758
1758
  })();
1759
1759
  }
1760
+ if (!hasMigration("043_node_suggestion_details")) {
1761
+ db.transaction(() => {
1762
+ db.exec(`
1763
+ DROP TABLE IF EXISTS node_suggestions;
1764
+ CREATE TABLE node_suggestions (
1765
+ nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
1766
+ node_id TEXT NOT NULL,
1767
+ suggestion_id TEXT NOT NULL,
1768
+ source TEXT NOT NULL,
1769
+ actor TEXT NOT NULL,
1770
+ detected_at TEXT NOT NULL,
1771
+ target_hash TEXT NOT NULL,
1772
+ proposed_hash TEXT NOT NULL,
1773
+ note TEXT,
1774
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1775
+ PRIMARY KEY (nest_id, node_id, suggestion_id)
1776
+ );
1777
+ CREATE INDEX IF NOT EXISTS idx_node_suggestions_nest_detected
1778
+ ON node_suggestions(nest_id, detected_at DESC);
1779
+ UPDATE nests SET index_synced_at = NULL;
1780
+ `);
1781
+ recordMigration("043_node_suggestion_details");
1782
+ })();
1783
+ }
1760
1784
  }
1761
1785
  function fkViolationCounts(db) {
1762
1786
  const rows = db.pragma("foreign_key_check");
@@ -2031,7 +2055,7 @@ async function initDb() {
2031
2055
  if (config.DB_DRIVER === "postgres") {
2032
2056
  const { Pool } = await import("pg");
2033
2057
  const { PostgresAdapter } = await import("./adapter.postgres-6VZCMPQL.js");
2034
- const { runPostgresMigrations } = await import("./migrations.postgres-APCVSYUE.js");
2058
+ const { runPostgresMigrations } = await import("./migrations.postgres-AIQ7WSU7.js");
2035
2059
  const pool = new Pool(buildPgConfig());
2036
2060
  adapter = new PostgresAdapter(pool);
2037
2061
  await runPostgresMigrations(adapter);