@promptowl/contextnest-community 1.24.0 → 1.26.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 (125) hide show
  1. package/API.md +177 -14
  2. package/CONFIGURATION.md +577 -116
  3. package/README.md +4 -3
  4. package/STEWARDSHIP.md +24 -0
  5. package/dist/{chunk-XUIWAWDO.js → chunk-2QOJE7A5.js} +4 -4
  6. package/dist/{chunk-2TPQTN4Y.js → chunk-DGPTJXIV.js} +47 -15
  7. package/dist/chunk-I5EDEZBQ.js +523 -0
  8. package/dist/{chunk-ZTT4U4NE.js → chunk-IEAWRHMV.js} +99 -12
  9. package/dist/{chunk-YVMSM7LS.js → chunk-LHWOJPFE.js} +7 -0
  10. package/dist/{chunk-5EZOPA47.js → chunk-M6PGYMBD.js} +20 -11
  11. package/dist/{chunk-F66VCBLA.js → chunk-PICUHQEM.js} +51 -2
  12. package/dist/{chunk-I3CSD6CK.js → chunk-RPGFHESR.js} +2 -2
  13. package/dist/{chunk-KIAAEHWL.js → chunk-SWA5M5XX.js} +88 -546
  14. package/dist/{chunk-LA3VTQ22.js → chunk-TRSGPS27.js} +81 -14
  15. package/dist/{engine-S3QBQ7LH.js → engine-QNMFDIBV.js} +4 -3
  16. package/dist/{external-edit-service-6FNFPOJJ.js → external-edit-service-4MEGPT4D.js} +5 -4
  17. package/dist/{grants-service-UT3EQ3R7.js → grants-service-24GLUBY4.js} +3 -3
  18. package/dist/index.js +1739 -583
  19. package/dist/invited-user-AM6A3WGO.js +12 -0
  20. package/dist/{migrations.postgres-AIQ7WSU7.js → migrations.postgres-HGSQ7IA5.js} +16 -2
  21. package/dist/{review-service-HHGOTO6P.js → review-service-2EWN7WWO.js} +8 -7
  22. package/dist/{stewardship-service-4DHNRULG.js → stewardship-service-TDZUC2Q6.js} +7 -4
  23. package/dist/{version-service-KIOQME6U.js → version-service-C3JTZLJH.js} +5 -4
  24. package/dist/web3/assets/ActivityTracePage-DzwWIjjx.js +1 -0
  25. package/dist/web3/assets/{AgentDocsPage-CIiaBqMy.js → AgentDocsPage-ixP2UdAE.js} +1 -1
  26. package/dist/web3/assets/CollaboratorManager-C9VAXZ0h.js +1 -0
  27. package/dist/web3/assets/CollaboratorsTab-tq4ZTexH.js +1 -0
  28. package/dist/web3/assets/DocumentEditor-C2CP2fiE.js +36 -0
  29. package/dist/web3/assets/DocumentsTab-Mh7KxUoB.js +6 -0
  30. package/dist/web3/assets/ExternalEditsTab-B_CWKXZ_.js +1 -0
  31. package/dist/web3/assets/MarkdownEditor-CKh4WSn-.js +653 -0
  32. package/dist/web3/assets/MarkdownEditor-ndtIx5x0.css +1 -0
  33. package/dist/web3/assets/NestPageHeader-Cizo8nUi.js +1 -0
  34. package/dist/web3/assets/NestView-EW-sraEb.js +73 -0
  35. package/dist/web3/assets/OverviewTab-CjidsMX_.js +6 -0
  36. package/dist/web3/assets/PersonCombobox-BKI7dff4.js +1 -0
  37. package/dist/web3/assets/{ReviewActions-OfOdqzCn.js → ReviewActions-CS8JXgXF.js} +2 -2
  38. package/dist/web3/assets/ReviewTab-CwVrXzw5.js +1 -0
  39. package/dist/web3/assets/StewardsTab-C-iP6gnh.js +1 -0
  40. package/dist/web3/assets/SubmitForReviewModal-Biq3m79i.js +1 -0
  41. package/dist/web3/assets/{alert-dialog-DTozKlmV.js → alert-dialog-BvF3NFr7.js} +2 -2
  42. package/dist/web3/assets/{arrow-left-BCKt4CwQ.js → arrow-left-CWIsv-sG.js} +1 -1
  43. package/dist/web3/assets/backlinks-Cqcn7n1J.js +19 -0
  44. package/dist/web3/assets/boxes-ClbgQqG2.js +6 -0
  45. package/dist/web3/assets/{chevron-left-BPwUT9DS.js → chevron-left-CK_ZPFm9.js} +1 -1
  46. package/dist/web3/assets/{circle-check-DI8IBGeo.js → circle-check-Bw_aTXiH.js} +1 -1
  47. package/dist/web3/assets/{circle-x-VKVhC88W.js → circle-x-DkIG68H7.js} +1 -1
  48. package/dist/web3/assets/client-attribution-BraarYaP.js +1 -0
  49. package/dist/web3/assets/{code-xml-CMabYZvH.js → code-xml-BlR7EN6l.js} +1 -1
  50. package/dist/web3/assets/{corner-down-right-BGxwK6wH.js → corner-down-right-DSVxK3tf.js} +1 -1
  51. package/dist/web3/assets/count-skeleton-DToLM74B.js +1 -0
  52. package/dist/web3/assets/{earth-BEp1EKTo.js → earth-BXhVPvCR.js} +1 -1
  53. package/dist/web3/assets/{file-exclamation-point-r8qZGIjP.js → file-exclamation-point-BevGZgtj.js} +1 -1
  54. package/dist/web3/assets/{folder-input-Cmtx1Jhk.js → folder-input-m8UBV4fA.js} +1 -1
  55. package/dist/web3/assets/{index-C1wTSjfP.js → index-BOVYgMvj.js} +1 -1
  56. package/dist/web3/assets/index-DVnaYqj9.css +1 -0
  57. package/dist/web3/assets/index-WYDmEOfO.js +404 -0
  58. package/dist/web3/assets/page-B6pkc9cd.js +2 -0
  59. package/dist/web3/assets/page-BJrFCKXB.js +1 -0
  60. package/dist/web3/assets/page-Bgy9Ng9p.js +1 -0
  61. package/dist/web3/assets/page-C2zoxumw.js +1 -0
  62. package/dist/web3/assets/page-CJWO06dN.js +1 -0
  63. package/dist/web3/assets/page-CptuAPJc.js +6 -0
  64. package/dist/web3/assets/page-DPY01SOj.js +1 -0
  65. package/dist/web3/assets/page-Dea2SblF.js +9 -0
  66. package/dist/web3/assets/page-Deq6IKiN.js +6 -0
  67. package/dist/web3/assets/page-Dfjg8p7Z.js +1 -0
  68. package/dist/web3/assets/{page-ByyVLDVo.js → page-Dh57L0RG.js} +5 -5
  69. package/dist/web3/assets/page-Dwbp4Seu.js +6 -0
  70. package/dist/web3/assets/{page-CHb0F5hV.js → page-DzKwP5Ds.js} +3 -3
  71. package/dist/web3/assets/page-YxjrE5b5.js +45 -0
  72. package/dist/web3/assets/{page-title-ClvvBsIo.js → page-title-DVq4qvMR.js} +1 -1
  73. package/dist/web3/assets/{play-C5YNKpAH.js → play-CXhS6Pgq.js} +1 -1
  74. package/dist/web3/assets/{send-0CHvmluT.js → send-DdfwnIrj.js} +1 -1
  75. package/dist/web3/assets/{settings-CeeF4LEb.js → settings-DKX0Q4gO.js} +1 -1
  76. package/dist/web3/assets/{share-2-CMnOlOt-.js → share-2-CxmXRt58.js} +1 -1
  77. package/dist/web3/assets/{tag-DQ_6J5Gv.js → tag-DeYC_mRv.js} +1 -1
  78. package/dist/web3/assets/{trash-2-HBi-Pslz.js → trash-2-B0HyPTnI.js} +1 -1
  79. package/dist/web3/assets/{user-plus-CvBNcpn4.js → user-plus-Ckm_Isyc.js} +1 -1
  80. package/dist/web3/assets/{x-By7piikG.js → x-mNtADYHc.js} +1 -1
  81. package/dist/web3/assets/zap-D2ifcnYb.js +16 -0
  82. package/dist/web3/index.html +2 -2
  83. package/package.json +3 -2
  84. package/dist/client-KEY4PYJH.js +0 -11
  85. package/dist/keys-CTOGAG3W.js +0 -20
  86. package/dist/web3/assets/ActivityTracePage-BOZHgJ8S.js +0 -1
  87. package/dist/web3/assets/CollaboratorManager-By91wrIr.js +0 -1
  88. package/dist/web3/assets/CollaboratorsTab-D1EE64Fs.js +0 -1
  89. package/dist/web3/assets/DocumentEditor-DrQfrW3d.js +0 -36
  90. package/dist/web3/assets/DocumentsTab-Vba-AzBv.js +0 -6
  91. package/dist/web3/assets/ExternalEditsTab-BSzAQIGM.js +0 -1
  92. package/dist/web3/assets/MarkdownEditor-74N_naNQ.css +0 -1
  93. package/dist/web3/assets/MarkdownEditor-C9zqZjC5.js +0 -643
  94. package/dist/web3/assets/NestPageHeader-CoLUV1Oz.js +0 -1
  95. package/dist/web3/assets/NestView-CTZ55tXD.js +0 -63
  96. package/dist/web3/assets/OverviewTab-Dxi-Hu-N.js +0 -1
  97. package/dist/web3/assets/PersonCombobox-CPOlNs6G.js +0 -1
  98. package/dist/web3/assets/ReasonDialog-DgLxfRWv.js +0 -1
  99. package/dist/web3/assets/ReviewTab-CyX8yxd3.js +0 -1
  100. package/dist/web3/assets/StewardsTab-DBEEfAQI.js +0 -1
  101. package/dist/web3/assets/SubmitForReviewModal-Ci2DdWs0.js +0 -1
  102. package/dist/web3/assets/backlinks-CYd4xENL.js +0 -24
  103. package/dist/web3/assets/card-XG2dP5Xf.js +0 -1
  104. package/dist/web3/assets/count-skeleton-DMbdpscX.js +0 -1
  105. package/dist/web3/assets/dates-BCxbm4_q.js +0 -1
  106. package/dist/web3/assets/index-BM-h3DwI.css +0 -1
  107. package/dist/web3/assets/index-EaX2yql0.js +0 -389
  108. package/dist/web3/assets/page-B3yvQvHy.js +0 -1
  109. package/dist/web3/assets/page-BExMvttJ.js +0 -1
  110. package/dist/web3/assets/page-BUOADv2X.js +0 -1
  111. package/dist/web3/assets/page-BYUFhYWZ.js +0 -1
  112. package/dist/web3/assets/page-Bicf02Cz.js +0 -1
  113. package/dist/web3/assets/page-BsPMDm5d.js +0 -45
  114. package/dist/web3/assets/page-CC0NJi8v.js +0 -1
  115. package/dist/web3/assets/page-CPjWQNKx.js +0 -11
  116. package/dist/web3/assets/page-CQt3ZGbf.js +0 -2
  117. package/dist/web3/assets/page-Cj1NetQ-.js +0 -1
  118. package/dist/web3/assets/page-CtaW65El.js +0 -1
  119. package/dist/web3/assets/page-DGOL9l8i.js +0 -6
  120. package/dist/web3/assets/refresh-cw-BhHzSH9m.js +0 -6
  121. package/dist/web3/assets/scroll-area-dRWncRqa.css +0 -1
  122. package/dist/web3/assets/scroll-area-hz-xtayP.js +0 -1
  123. package/dist/web3/assets/select-DBFTrUmI.js +0 -6
  124. package/dist/web3/assets/triangle-alert-CDy8-7sv.js +0 -6
  125. package/dist/web3/assets/zap-C2T8riBp.js +0 -11
package/API.md CHANGED
@@ -12,8 +12,86 @@ Two methods, evaluated in order:
12
12
 
13
13
  Anonymous open mode (`AUTH_MODE=open`) bypasses auth on most read endpoints.
14
14
 
15
+ ### Nest-scoped API keys
16
+
17
+ A key minted with `nest_id` reaches **that one nest and nothing else**. It is deny-by-default: a request outside the list below answers **403** `{ "error": "This API key is scoped to one nest and can't …", "code": "nest_scoped_key" }`, before the route's own checks run, so a route added later is closed to nest keys until it is allowed on purpose.
18
+
19
+ | A nest-scoped key **can** reach | Notes |
20
+ |---|---|
21
+ | `/nests/<its nest>` and everything under it, including `/nests/<its nest>/mcp` | The owner's permission on that nest still applies. |
22
+ | `GET /nests`, `GET /search`, `GET /stats` | Narrowed to its nest — other nests never appear. |
23
+ | `/mcp`, `GET /index` | Already filtered to its nest. |
24
+ | `/runs/<id>/*` | Only runs in its nest (403 otherwise). |
25
+ | `GET /health`, `GET /license/status`, `GET /llms.txt`, `GET /auth/admin-status` | Public / self-describing. |
26
+
27
+ Everything else is **403 `nest_scoped_key`** — notably `/auth/keys*` (a nest key can't mint, rotate, list or revoke keys, so it can't trade itself for a whole-account key), `POST /nests`, every other nest (`/nests/<other>/…` answers 403 "API key not authorized for this nest"; its MCP transport answers with a JSON-RPC error envelope), `/admin/*`, `/auth/teammates`, `/auth/invite`, `/auth/users/*`, `/auth/admin/*`, `/auth/profile`, `/auth/password`, `/teams*`, `/people*` and `/me/*` — even when the key's owner is a server admin. A session cookie sent alongside the key wins, as everywhere else. User-level keys (no `nest_id`) and sessions are unaffected.
28
+
15
29
  All bodies are JSON unless noted. All errors return `{ "error": "msg" }` with appropriate HTTP status.
16
30
 
31
+ ## Caller attribution (`client`)
32
+
33
+ `edited_by` names a person and a read names nobody, so neither answers *which
34
+ agent, in which session*. Every write body below, and every MCP tool call, takes
35
+ an optional `client` object that does ([spec §9.4](https://github.com/PromptOwl/ContextNest/blob/main/CONTEXT_NEST_SPEC.md)):
36
+
37
+ ```json
38
+ { "title": "API Design", "content": "…",
39
+ "client": { "agent": "claude-code", "session_id": "s-9f2", "run": 7 } }
40
+ ```
41
+
42
+ - `agent` and `session_id` are reserved; any other key is custom and is recorded
43
+ verbatim. Values are scalars (string / number / boolean).
44
+ - A write that creates a version records it **on that version** — it comes back
45
+ on `GET .../versions` and on MCP `context_versions` (in the prose and the
46
+ structured payload). Every path that seals a version takes it: create, edit,
47
+ approve, revert, and import (`POST /nests/import`, `POST /nests/unsynced/sync`,
48
+ which stamp every row they seed), plus `POST /review-queue/bulk`, whose one
49
+ block rides every approval in the batch. `POST .../submit-review` seals it
50
+ into the engine's own version chain (`history.yaml`), which is the entry that
51
+ submission appends. Every MCP tool call, and every REST
52
+ call that accepts `client`, records it on the activity trace
53
+ (`GET /admin/trace`, `GET /nests/:nestId/trace`, where it comes back as a
54
+ parsed `client` object) — which is where a call that seals no version is
55
+ attributed: a **read**, and a **rejection**, which commits no content.
56
+ - **Entirely optional.** No endpoint requires it and an unattributed call is a
57
+ valid call. A call that carries none stores nothing rather than an empty
58
+ object — "not attributed" and "attributed to nobody" are different claims.
59
+ - **A label, not an identity.** It is never authenticated, never used to
60
+ authorize, and never an input to a hash chain, so histories written before the
61
+ field existed keep verifying byte for byte. `editedBy` remains the
62
+ authoritative authoring record.
63
+ - **Bounded**, because an untrusted caller writes it into an append-only audit
64
+ trail: values ≤512 chars, ≤16 custom keys, scalars only → `400` otherwise. A
65
+ near-miss on a reserved key (`sessionId`, `Agent`) is refused rather than
66
+ filed as a custom key, which would look attributed and not be.
67
+ - The server fills what the transport already knows, **per key, under** anything
68
+ the caller sent. Precedence is caller > env > connection: on MCP the
69
+ connection means the `initialize` handshake's `clientInfo.name` and the
70
+ transport's session id; behind that sits the label the operator gave the API
71
+ key. `CONTEXTNEST_AGENT` / `CONTEXTNEST_SESSION_ID` are the env layer;
72
+ `CONTEXTNEST_NO_ATTRIBUTION=1` derives nothing (a `client` the caller sent
73
+ explicitly is still honoured). See `CONFIGURATION.md`.
74
+ - Note the `/mcp` endpoints are **stateless** — one server per request, no
75
+ session issued — so a handshake's `clientInfo` is not in scope by the time a
76
+ tool call arrives, and `agent` comes from the key label in practice. An agent
77
+ that wants to name itself exactly should send `client` rather than rely on
78
+ what the connection happens to report.
79
+
80
+ - **Treat a recorded `client` as untrusted text when you read it back.** The
81
+ values are whatever the caller sent, stored verbatim and unescaped by design —
82
+ an audit record that rewrites what the caller said is worth less than none —
83
+ and `context_versions` prints `agent` into the prose an LLM client reads
84
+ (`[via claude-code]`). That is the same standing as every other caller-written
85
+ string this server serves back: document titles, bodies, change notes and
86
+ review notes all reach an agent's context the same way. Bounds (≤512 chars,
87
+ scalars) cap the size, not the content. Render it as data, never as
88
+ instructions, and the usual rule applies to anything downstream: escape at the
89
+ point of use. This server's own UI does — React escapes it as a text node, and
90
+ nothing interpolates it into SQL (`client_json` is always a bound parameter).
91
+
92
+ Distinct from `metadata`, which is frontmatter: `metadata` describes the
93
+ document, `client` describes the call that touched it.
94
+
17
95
  ---
18
96
 
19
97
  ## 1. Auth
@@ -57,16 +135,16 @@ Mints an API key for the caller. A user may hold **several** keys — one per pl
57
135
  ```
58
136
  Response (201):
59
137
  ```json
60
- { "api_key": "cnst_...", "id": "uuid", "key_prefix": "cnst_abc123...", "label": "cli" }
138
+ { "api_key": "cnst_...", "id": "uuid", "key_prefix": "cnst_abc123...", "label": "cli", "nest_id": null }
61
139
  ```
62
- **Plaintext shown once.** Omit `nest_id` for a **user-level key** (works on every nest the user can access — what the Connect dialog mints); a nest-scoped key is rejected on other nests' MCP/API paths.
140
+ **Plaintext shown once.** Omit `nest_id` for a **user-level key** (works on every nest the user can access — what the whole-brain Connect flow mints). With `nest_id` the key is **nest-scoped** — see [Nest-scoped API keys](#nest-scoped-api-keys) for exactly what it can reach. The caller must be able to reach that nest: an unknown nest, or one the caller has no access to, is **404** (the same answer for both, so this is no existence oracle). A nest-scoped key calling this endpoint gets **403 `nest_scoped_key`**.
63
141
 
64
142
  ### POST `/auth/keys/rotate` (auth required)
65
143
  Replaces **one** key and returns its new plaintext; the caller's other keys are untouched.
66
144
 
67
145
  `key_id` names the target. Omitting it is only accepted when the account holds exactly one key (pre-multi-key clients) — with several keys and no `key_id` the server answers **400** rather than guessing which to replace. **404** if the id isn't the caller's.
68
146
 
69
- Scope resolution on the optional `nest_id` field: **absent** → keep the prior scope; **`null` (or `""`)** → clear to a user-level key; **a nest id** → scope to that nest. Same for `label` (absent keeps the prior label). Supplying a `label` that matches a *different* key of the caller's is **409**; renaming a key to what it is already called is fine.
147
+ Scope resolution on the optional `nest_id` field: **absent** → keep the prior scope; **`null` (or `""`)** → clear to a user-level key; **a nest id** → scope to that nest (**404** unless the caller can reach it, and the old key is left alone). The response reports the new key's `nest_id`. Un-scoping has to come from a session or a user-level key — a nest-scoped key calling any `/auth/keys*` route gets **403 `nest_scoped_key`**. Same for `label` (absent keeps the prior label). Supplying a `label` that matches a *different* key of the caller's is **409**; renaming a key to what it is already called is fine.
70
148
  ```json
71
149
  { "key_id": "uuid", "label": "optional", "nest_id": "optional" }
72
150
  ```
@@ -95,8 +173,7 @@ Creates an invited user and returns a **temporary password** (once) for the admi
95
173
  { "email": "teammate@x.com" }
96
174
  ```
97
175
  - New email → user row with `is_invited = 1` and a temporary password.
98
- - Existing, not-yet-claimed email (`is_invited = 1`, hasn't logged in) → refreshes the temp password.
99
- - Existing, claimed email (has logged in or set their own password, `is_invited = 0`) → `409`; use Reset password instead.
176
+ - Any existing email → `409`, credentials untouched. The error says "already a member" once they've signed in (`is_invited = 0`), or "already invited but hasn't signed in yet" (`is_invited = 1`, including people a nest was shared with). Use Reset password to issue a new temporary password.
100
177
 
101
178
  `is_invited` clears to `0` on the teammate's first successful login (or when they set their own password).
102
179
 
@@ -105,9 +182,12 @@ Response (201):
105
182
  {
106
183
  "temporary_password": "...",
107
184
  "user": { "id": "...", "email": "..." },
185
+ "promptowl_sign_in": true,
186
+ "emailed": true,
108
187
  "message": "Share this temporary password securely — it won't be shown again."
109
188
  }
110
189
  ```
190
+ `emailed` — email is configured, so the invite email (password + sign-in link) was sent. `promptowl_sign_in` — PromptOwl sign-in is open to everyone, so the teammate may use it instead (the email says so too).
111
191
 
112
192
  ### GET `/auth/teammates` (admin only)
113
193
  Returns all users + key counts. **Sorted: admins first, then `created_at DESC`.**
@@ -157,12 +237,12 @@ Starts generic **OIDC single sign-on** (Microsoft Entra ID, Google, or any spec-
157
237
  When enabled and fully configured: fetches the issuer's discovery document (cached in-process ~5 min), mints `state` + `nonce` + a PKCE S256 verifier into three short-lived (10 min) httpOnly `SameSite=Lax` cookies scoped to `Path=/auth/oidc`, and `302`s to the IdP's authorization endpoint with `response_type=code&scope=openid email profile`. When disabled/unconfigured → `302` to `/?sso_error=disabled` / `not_configured`. Rate-limited per IP.
158
238
 
159
239
  ### GET `/auth/oidc/callback`
160
- The redirect URI (`<base url>/auth/oidc/callback` — register this exact URI at your IdP). Verifies the `state` param against the flow cookie, exchanges the code at the token endpoint (client secret + PKCE verifier), validates the ID token with the issuer's JWKS (signature, `iss`, `aud` = client id, `exp`, and the `nonce` claim against the flow cookie), extracts the email (`email`, falling back to `preferred_username` for Entra; normalized trim+lowercase), and enforces `oidc_allowed_domains`. Existing users are signed in; unknown users are JIT-provisioned when `oidc_auto_provision` is on (name from the `name` claim), else refused. Every successful login also upserts `users.department` from the optional string `department` claim (trimmed, whitespace collapsed, capped at 120 chars; absent claim clears the stored value) — read by the `oidc_department_tagging` feature (see `CONFIGURATION.md → Department auto-tagging`). On success → session cookie (identical to a password login) + `302` to `/`. On any failure → `302` to `/?sso_error=<code>`; codes: `disabled`, `not_configured`, `discovery_failed`, `provider_error`, `state_mismatch`, `exchange_failed`, `invalid_token`, `email_not_verified` (ID token carries `email_verified: false` — an absent claim is trusted; see the trust assumption in `CONFIGURATION.md → Single sign-on (OIDC)`), `domain_not_allowed`, `not_invited`, `rate_limited`, `service_error`. Flow cookies are cleared on every outcome. Rate-limited per IP.
240
+ The redirect URI (`<base url>/auth/oidc/callback` — register this exact URI at your IdP). Verifies the `state` param against the flow cookie, exchanges the code at the token endpoint (client secret + PKCE verifier), validates the ID token with the issuer's JWKS (signature, `iss`, `aud` = client id, `exp`, and the `nonce` claim against the flow cookie), extracts the email (`email`, falling back to `preferred_username` for Entra; normalized trim+lowercase), and enforces `oidc_allowed_domains`. Existing users are signed in; unknown users are JIT-provisioned when `oidc_auto_provision` is on (name from the `name` claim), else refused. Every successful login also upserts `users.department` from the optional string `department` claim (trimmed, whitespace collapsed, capped at 120 chars; absent claim clears the stored value) — read by the `oidc_department_tagging` feature (see `CONFIGURATION.md → Department auto-tagging`). On success → session cookie (identical to a password login) + `302` to `/`. On any failure → `302` to `/?sso_error=<code>`; codes: `disabled`, `not_configured`, `discovery_failed`, `provider_error`, `state_mismatch`, `exchange_failed`, `invalid_token`, `email_not_verified` (ID token carries `email_verified: false` — an absent claim is trusted; see the trust assumption in `CONFIGURATION.md → Single sign-on (OIDC)`), `domain_not_allowed`, `not_invited`, `rate_limited`, `service_error` (cause and fix for each: `CONFIGURATION.md → Troubleshooting SSO sign-in`). Flow cookies are cleared on every outcome. Rate-limited per IP.
161
241
 
162
242
  ### GET `/auth/oidc/logout`
163
243
  RP-initiated logout for an SSO session. Deletes the current session server-side and clears the session cookie plus the `cnst_sso_session` marker. If the issuer advertises an `end_session_endpoint` (Entra/Okta/Keycloak; Google does not), `302`s the browser through it with `post_logout_redirect_uri=<base url>/` so the IdP session ends too; otherwise `302`s straight to `/`. Always clears cookies and always lands on `/`, even if discovery is unreachable. The SPA navigates here (instead of `POST /auth/logout`) only when the non-httpOnly `cnst_sso_session` marker cookie — set on OIDC callback success — is present, so password sessions keep the plain logout.
164
244
 
165
- Related `/admin/settings` fields (GET/PATCH, superadmin only): `oidc_enabled`, `oidc_issuer` (https URL), `oidc_client_id`, `oidc_client_secret` (**write-only** — PATCH sets/replaces, `null` clears; GET returns only `oidc_client_secret_set: boolean`), `oidc_allowed_domains` (comma-separated), `oidc_auto_provision`, `oidc_department_tagging` (append `dept:<slugified-department>` to a new document's tags on create — create-only, no-op for users without a stored department). Enabling is refused unless issuer + client id + secret are all present.
245
+ Related `/admin/settings` fields (GET/PATCH, server admin only — the license admin or a superadmin): `oidc_enabled`, `oidc_issuer` (https URL), `oidc_client_id`, `oidc_client_secret` (**write-only** — PATCH sets/replaces, `null` clears; GET returns only `oidc_client_secret_set: boolean`), `oidc_allowed_domains` (comma-separated), `oidc_auto_provision`, `oidc_department_tagging` (append `dept:<slugified-department>` to a new document's tags on create — create-only, no-op for users without a stored department). Enabling is refused unless issuer + client id + secret are all present.
166
246
 
167
247
  ### POST `/auth/token-exchange`
168
248
  Exchange an IdP ID token for a short-lived, scoped **MCP bearer** (RFC 8693-shaped) — for an external agent acting on behalf of a user your IdP already authenticated. No session, no API key. **Off by default; security-critical** — see `CONFIGURATION.md → Agent SSO (token exchange)`.
@@ -182,7 +262,7 @@ Related `/admin/settings` fields (GET/PATCH, superadmin only): `sso_token_exchan
182
262
  A nest = a context vault (folder of markdown documents + governance state).
183
263
 
184
264
  ### GET `/nests`
185
- Returns owned + shared nests. Archived nests are **absent** — as they are from every other listing, the work inbox, cross-nest search and the MCP index. Each row carries `pinned` — the calling user's own bookmark (see `POST /nests/:nestId/pin`).
265
+ Returns owned + shared nests. Archived nests are **absent** — as they are from every other listing, the work inbox, cross-nest search and the MCP index. Each row carries `pinned` — the calling user's own bookmark (see `POST /nests/:nestId/pin`). Nests reachable through `org`/`public` visibility alone are included too and carry `discovered: true` (owned and shared rows carry `false`) — the dashboard's default *My nests* scope leaves them out.
186
266
  ```json
187
267
  { "nests": [{ "id": "...", "user_id": "...", "name": "...", "slug": "...", "visibility": "private", "created_at": "..." }] }
188
268
  ```
@@ -195,6 +275,8 @@ Returns owned + shared nests. Archived nests are **absent** — as they are from
195
275
  ```
196
276
  Response (201): `{ "nest": { ... } }`
197
277
 
278
+ With `NEST_CREATION=admins` (the default) only a server admin may create or import a nest; anyone else gets `403` ("Only a server admin can create nests on this server…"). The same rule covers `POST /nests/import` and the MCP `nest_create` tool. `GET /health` reports it as `nest_creation`.
279
+
198
280
  ### GET `/nests/:nestId`
199
281
  ```json
200
282
  { "nest": { ... }, "permission": "owner" | "admin" | "write" | "read" }
@@ -235,7 +317,8 @@ Pins are per `(user, nest)` in the `nest_pins` table and cascade with the nest.
235
317
  "stewardship_enabled": false,
236
318
  "allow_self_approve": false,
237
319
  "prime_only_review": false,
238
- "prime_tags": ["prime-document"]
320
+ "prime_tags": ["prime-document"],
321
+ "creator_is_reviewer": false
239
322
  }
240
323
  ```
241
324
 
@@ -249,6 +332,7 @@ Every field is optional; only the ones present are changed.
249
332
  - `allow_self_approve` — owner/admin writes publish immediately instead of drafting.
250
333
  - `prime_only_review` — in a governed nest, only **prime** documents need approval; everything else self-publishes. Off by default.
251
334
  - `prime_tags` — tags that mark a document prime. Accepts an array or a comma-separated string; stored and returned normalized (no leading `#`, lowercased, deduped). See `STEWARDSHIP.md → Prime documents`.
335
+ - `creator_is_reviewer` — in a governed nest, whoever creates a document becomes its document-scope `reviewer` and v1 publishes immediately; later changes go through review as usual (the creator approves teammates' changes, another reviewer approves the creator's). Prime documents still draft. Future creates only. Off by default. See `STEWARDSHIP.md → Creators review their own documents`.
252
336
 
253
337
  Turning stewardship **off** wipes stewards and pending reviews and is owner-only; the other fields are non-destructive and never change the state of anything already published.
254
338
 
@@ -264,7 +348,7 @@ Turning stewardship **off** wipes stewards and pending reviews and is owner-only
264
348
  | `public` | anyone, including anonymous callers |
265
349
  `org` and `public` readers see **approved content only** — drafts and pending versions stay with collaborators and stewards.
266
350
  Under `AUTH_MODE=open` every caller resolves to the anonymous user, so there is no "other authenticated user" for `org` to admit: on an open-mode deployment `org` behaves as `private`. Use it on a deployment with real logins.
267
- - **Outside-org publish guardrail.** Setting `visibility: "public"` **requires** `acknowledge_public: true` (`org` does not — it stays inside the deployment, so it crosses no organizational boundary). Without it the request is rejected `409`:
351
+ - **Outside-org publish guardrail.** Setting `visibility: "public"` **requires** `acknowledge_public: true`. `org` does not on a single-organization server — it stays inside the deployment, so it crosses no organizational boundary. On a **shared deployment** (`SHARED_DEPLOYMENT=true` / Settings → General → *This server hosts several organizations* — the PromptOwl-hosted service) the deployment *is* several organizations, so `org` requires the same `acknowledge_public: true` and the `warning` names that boundary. Without the acknowledgement the request is rejected `409`:
268
352
  ```json
269
353
  {
270
354
  "error": "Publishing this nest requires acknowledgement.",
@@ -272,7 +356,15 @@ Turning stewardship **off** wipes stewards and pending reviews and is owner-only
272
356
  "warning": "This makes the nest readable by anyone on the internet, outside your organization."
273
357
  }
274
358
  ```
275
- Going back to `private` needs no acknowledgement.
359
+ On a shared deployment, `org` without the acknowledgement is rejected the same way, with its own warning:
360
+ ```json
361
+ {
362
+ "error": "Opening this nest to the organization requires acknowledgement.",
363
+ "requires_acknowledgement": true,
364
+ "warning": "This server is shared: 'Organization' makes the nest readable by every account on this server, including other organizations — not just yours."
365
+ }
366
+ ```
367
+ Going back to `private` needs no acknowledgement. `GET /health` reports `shared_deployment` so a client can word its own confirmation the same way.
276
368
 
277
369
  ### PATCH `/nests/:nestId/reader-mode` (server-admin / superadmin only)
278
370
  Configure **Public Docs Reader Mode** — a clean, read-only public docs view of the nest. Superadmin-gated via `isServerAdminUserId` (license admin OR `access.yaml` super_admins ∪ the DB-granted set; the same gate as `/nests/unsynced` and `/admin/*`). A plain nest-owner/admin who is not a superadmin gets `403`.
@@ -297,6 +389,41 @@ Response (200) echoes the persisted row:
297
389
  ```
298
390
  The two additive columns (`nests.reader_mode`, `nests.reader_home_node`) also surface on `GET /nests/:nestId` via the `SELECT *` nest payload.
299
391
 
392
+ ### PATCH `/nests/:nestId/branding` (nest owner / nest admin / server admin)
393
+ Per-nest branding for the public reader (reader mode) and the nest's `/p` published pages. The body **replaces** the nest's branding; `null`, an empty body, or `{}` resets it to unbranded.
394
+ ```json
395
+ {
396
+ "site_title": "Acme Docs",
397
+ "logo_url": "/nests/<nestId>/assets/<uuid>.png",
398
+ "banner_url": "https://cdn.example.com/banner.jpg",
399
+ "banner_headline": "Acme Handbook",
400
+ "banner_subtext": "Everything we know, versioned.",
401
+ "accent_color": "#e11d48",
402
+ "header_bg_color": "#0f172a",
403
+ "page_tint_color": "#fef2f2",
404
+ "footer_text": "© Acme Corp",
405
+ "footer_link_url": "https://acme.example.com/",
406
+ "footer_link_label": "acme.example.com",
407
+ "favicon_url": "/nests/<nestId>/assets/<uuid>.png",
408
+ "hide_powered_by": false
409
+ }
410
+ ```
411
+ Every field is optional. Rules (any violation → `400`, nothing saved):
412
+ - **Colors** (`accent_color`, `header_bg_color`, `page_tint_color`): `#rgb` or `#rrggbb` only; stored as lowercase `#rrggbb`. Anything else — named colors, `red; background:url(…)`, `var()` — is refused.
413
+ - **Images** (`logo_url`, `banner_url`, `favicon_url`): an image asset uploaded to **this** nest (`POST /nests/:nestId/assets` → `/nests/<nestId>/assets/<uuid>.<png|jpg|gif|webp>`, must exist), or an `https://` URL (no credentials) whose path ends in `.png`, `.jpg`, `.jpeg`, `.gif` or `.webp`. `javascript:`, `data:`, `http:`, protocol-relative, SVG, video assets and other nests' assets are refused.
414
+ - **`footer_link_url`**: an `https://` URL.
415
+ - **Text** (`site_title` ≤ 120, `banner_headline` ≤ 120, `banner_subtext` ≤ 280, `footer_text` ≤ 280, `footer_link_label` ≤ 80): plain text, no control characters; always rendered escaped.
416
+ - **Unknown keys** are refused — there is no free-form CSS or JS.
417
+ - **`hide_powered_by`** (boolean): hides "Powered by ContextNest" in the reader and "Secured by PromptOwl · ContextNest" on `/p`. Turning it on is **server-admin only** (`403` for a nest owner/admin). A non-admin edit that omits the field keeps the stored value — a non-admin full clear (`null` / `{}`) included, which then stores `{ "hide_powered_by": true }`; a non-admin may turn it off explicitly.
418
+
419
+ Gate: the access middleware requires the `admin` tier for this resource (owner, admin collaborator, or server admin); the handler re-checks it. Editors and viewers get `403`, anonymous callers `401`.
420
+
421
+ Response (200): `{ "branding": { …normalised… } }` or `{ "branding": null }`.
422
+
423
+ **Reading it.** There is no separate GET: `GET /nests/:nestId` returns `nest.branding` (parsed and re-validated against the write rules — a stored URL that no longer passes is dropped; `null` when unbranded) in place of the raw `branding_json` column. It is therefore readable exactly by whoever can read the nest — an anonymous reader of a public nest sees it; a private nest still `404`s.
424
+
425
+ **Where it applies.** The SPA reader (read-only callers in reader mode, and an owner's "Preview as reader") uses it for the header logo + title (falling back to the server `LOGO_URL` and the nest name), the banner on the home page, accent/header/page-tint colors, footer, favicon and tab title. On `/p`, the markdown document view, the gate page and the comment shell pick up the colors, title, logo, banner and footer; **the CSP is unchanged** — uploaded images are inlined as `data:` URIs (already allowed by `img-src data:`), and external https images are not rendered there. HTML/artifact publishes are served untouched.
426
+
300
427
  ### GET `/nests/unsynced` (server-admin only outside open mode)
301
428
  Lists folders under `DATA_ROOT` that aren't nests yet — typically dropped in by the starter CLI or copied in manually. Each entry is a sync candidate. Reflects server-side filesystem state, so in non-open mode only a server admin — the license admin or any superadmin (`access.yaml` ∪ granted; same resolution as `/admin/*`) — sees it (everyone else gets `403`).
302
429
 
@@ -635,7 +762,8 @@ Optional query params, applied before the per-node enrichment:
635
762
  "scope": "team",
636
763
  "status": "draft",
637
764
  "folder": "gtm/deals",
638
- "schedule": "0 6 * * *"
765
+ "schedule": "0 6 * * *",
766
+ "client": { "agent": "claude-code", "session_id": "s-9f2" }
639
767
  }
640
768
  ```
641
769
  - Slug derived from title → `nodes/<slug>`.
@@ -644,6 +772,7 @@ Optional query params, applied before the per-node enrichment:
644
772
  - Optional `schedule` (free-form cron/interval string, opaque to the server) — **only valid when `type` is `agent` or `skill`**; sent on any other type → `400`. Stored in frontmatter metadata and echoed back on every node response. See [§9 `GET /runnables`](#get-nestsnestidrunnables-read).
645
773
  - Optional `prime` (boolean) flags the document as **prime** — it then always requires steward approval, whoever writes it. Owner/admin only; any other caller sending the field gets `403`. Omit to inherit from the nest's `prime_tags`; `false` exempts this document from an otherwise-prime tag. Echoed back on every node response as `prime` (absent when inherited). See `STEWARDSHIP.md → Prime documents`.
646
774
  - If stewardship enabled → status defaults `draft`. Else `approved`.
775
+ - Optional `client` — caller attribution, recorded on the version this create seals. See [Caller attribution](#caller-attribution-client).
647
776
  - Auto-syncs tag index for steward resolution.
648
777
 
649
778
  Response (201): `{ "node": { ... }, "stewards": [...] }`.
@@ -705,10 +834,12 @@ Headers (optional): `X-Base-Version: 3` — server returns 409 on conflict.
705
834
  "content": "...",
706
835
  "tags": ["..."],
707
836
  "changeNote": "fixed typo",
708
- "schedule": "0 7 * * *"
837
+ "schedule": "0 7 * * *",
838
+ "client": { "agent": "claude-code", "session_id": "s-9f2" }
709
839
  }
710
840
  ```
711
841
  Requires nest write permission.
842
+ - `client` — caller attribution, recorded on the version this edit seals. See [Caller attribution](#caller-attribution-client).
712
843
  - `schedule` — runnable types (`agent`/`skill`) only; sent for any other type → `400`. Send `""` to clear it.
713
844
  - `type` — re-type the node in place (the editor's "Turn into…"): `document`, `artifact`, `table`, `agent`, `skill`, `tool`, or any other engine type. Same gate as create — the artifact/table feature flags and the accepted set (`400` otherwise). The body is kept as-is. Converting to `skill` defaults the trigger from the title; converting away from a runnable type drops its `schedule`.
714
845
  - `prime` — owner/admin only (`403` otherwise). `true`/`false` set the document-scope flag, `null` clears it back to tag/nest inheritance. An edit that makes the document prime is itself gated, so it lands as a draft awaiting approval.
@@ -720,6 +851,23 @@ A node with a pending review is frozen — `423` — for callers who can't clear
720
851
 
721
852
  Purges everything keyed to the node id — version history, review requests, the approved-version pin, tag index, annotations and their comments, review comments, watchers, schedules, trigger hooks, workflow edges, document-scoped stewards and document grants — and leaves a deletion tombstone for `GET /changes`. Node ids derive from titles, so a later document can land on the same id; it must inherit none of the old one's readers, reviewers or history. Run traces are kept: they record what happened, not what exists. `POST /nests/:nestId/nodes/:nodeId/move` carries that same set to the new id instead of dropping it (folder grants excepted — they cover a path prefix, so a moved doc leaves one share and joins another).
722
853
 
854
+ ### POST `/nests/:nestId/nodes/pdf`
855
+ Upload a PDF as a governed `pdf` document. Multipart: `file` (required), optional `title` (default: the file name), `folder`, `tags` (repeat the field or comma-separate), `nodeId`, `changeNote`. Write permission (nest-scope steward editors too, as for `POST /nodes`).
856
+
857
+ The engine's `context_import_pdf` extracts the text — the node body, one `<!-- page N -->` marker per page — and stores the PDF beside the node as `<id>.pdf`, bound by sha256 in the node's `pdf:` frontmatter block. Governance is exactly a create's: in a governed nest the upload lands as a draft (or publishes when a create by that person would); without stewards it publishes.
858
+
859
+ - `nodeId` of an existing `pdf` node → the PDF is replaced as a **new version** of that node (same review lock, draft/publish rule and version rows as `PATCH`). The prior binary stays in history. Identical bytes are a no-op. A replace is two engine writes (stage the new PDF, then the same update/publish a text edit makes); if the second fails, the node's previous file and PDF are put back before the error is returned.
860
+ - `400` — no `file`, not a PDF (checked by the `%PDF-` header, not the extension), an encrypted/unreadable PDF, or `nodeId` names a node that is not a pdf. `413` — larger than `PDF_MAX_MB` (default 25 MB). Nothing is written on any refusal.
861
+ - A PDF with no text layer (a scan) imports with an empty body and `pdf.text_layer: false`; there is no OCR.
862
+ - Returns `201` `{ node, version, stewards? }` on create, `200` `{ node, version }` on replace. `node.pdf` is the `pdf:` block (`file`, `sha256`, `bytes`, `pages`, `text_layer`, `extractor`, `extractor_version`, `extracted_at`).
863
+
864
+ A pdf node's text is read-only: `PATCH` with a `content`/`append` that changes it → `409` (re-upload instead; echoing the stored body back is accepted). A pdf node can't be converted to another type or another type to `pdf` (`400`), can't be moved to another folder yet (`400`), and `POST /nodes` with `type: "pdf"` → `400`. `POST …/revert` re-imports the target version's PDF; `POST …/discard` restores the sealed version's PDF (the response carries a `warning` if that binary can't be restored); `DELETE` removes the binary with the node.
865
+
866
+ ### GET `/nests/:nestId/nodes/:nodeId/pdf`
867
+ The PDF binary, gated exactly like `GET /nodes/:nodeId`. A public reader (or `?approved_only=1`) always gets the **approved** version's bytes — never a draft's — whatever version is asked for; other readers get the current working copy, or `?version=N`. Every response is verified against the sha256 its version records (a mismatch is refused, not served).
868
+
869
+ Headers: `Content-Type: application/pdf`, `Content-Disposition: inline; filename="<slug>.pdf"` (`?download=1` → `attachment`), `Content-Security-Policy: sandbox`, `X-Content-Type-Options: nosniff`, `Cache-Control: private, no-cache`, and an `ETag` of the sha256 (`If-None-Match` → `304`). `404` when the node has no such version or no approved version (public). An id that is not a pdf node falls through to the node route, so a document whose own id ends in `/pdf` still reads.
870
+
723
871
  ### POST `/nests/:nestId/assets`
724
872
  Multipart upload (field `file`) of an image or video referenced from documents. Write permission.
725
873
 
@@ -1041,6 +1189,12 @@ The UI deep-links a comment item to its document with the thread focused via
1041
1189
 
1042
1190
  Each row carries `nest_id`, `nest_name` (resolved in the same read, so the inbox navigates without fetching the nest list), `kind`, `message`, `created_at`, `read_at`, and `node_id` when the row is about a document (review verdicts, document deletion flags, mentions); nest-level rows have `node_id: null`.
1043
1191
 
1192
+ Unread `nest_invite` rows are what the Nests page shows as **Pending invites**. Accept marks them read (`POST /me/notifications/read`) and opens the nest.
1193
+
1194
+ ### POST `/me/nests/:nestId/leave`
1195
+
1196
+ Decline a nest invite. Removes the caller's **own** collaborator row on that nest (never anyone else's) and marks its `nest_invite` rows read. Response: `{ "left": true }`. Access that isn't a direct collaborator row — ownership, or a share through a team — isn't touched: `{ "left": false }`, and the invite rows are still marked read.
1197
+
1044
1198
  ### GET `/me/drafts`
1045
1199
 
1046
1200
  Query: `?limit=25&offset=0&q=&sort=updated|title` (limit clamps to 1–100, offset to ≥ 0)
@@ -1103,6 +1257,11 @@ Each version may also include `resolvedBy` + `resolutionStatus`, the steward who
1103
1257
  approved or rejected it, joined from `review_requests` (`node_versions` stores
1104
1258
  the status but not the resolver's email).
1105
1259
 
1260
+ `client` is the caller attribution the write carried — which agent, in which
1261
+ session (see [Caller attribution](#caller-attribution-client)). Absent on every
1262
+ version whose write sent none, which includes every version written before the
1263
+ field existed.
1264
+
1106
1265
  `externalEditVerdicts` carries the verdicts on out-of-band (direct filesystem)
1107
1266
  edits, oldest first, read from the engine's append-only chain-event log. They sit
1108
1267
  beside the version list rather than inside it because a **rejection commits no
@@ -1127,6 +1286,7 @@ approved version.
1127
1286
  "status": "pending_review",
1128
1287
  "changeNote": "...",
1129
1288
  "content": "",
1289
+ "client": { "agent": "claude-code", "session_id": "s-9f2" },
1130
1290
  "diff": "Index: v2\n===...\n--- v2\tv2\n+++ v2\tv3\n@@ -1,4 +1,5 @@\n title: doc\n-old line\n+new line\n"
1131
1291
  },
1132
1292
  {
@@ -1433,7 +1593,7 @@ Body (give `prompt` OR `selector`; everything else optional):
1433
1593
  ```
1434
1594
  - `prompt` — natural language. Tokens are matched against tag names and titles, and every document is scored against them (title hits weigh most, then tags, then body words); documents under half the top score are dropped as noise. A prompt that matches nothing returns `nodes: []` — never the whole nest. `trace.compiled.fallback` is `"fulltext"` when body scoring alone produced the answer.
1435
1595
  - `selector` — explicit selector grammar (e.g. `#auth`). Wins over `prompt`.
1436
- - `hops` — wikilink expansion depth. Defaults to `0` for a `prompt` (neighbours of a keyword match are not answers to the question) and `2` for a `selector`.
1596
+ - `hops` — link expansion depth (`[[wikilinks]]` and `contextnest://` links, both directions). Defaults to `0` for a `prompt` (neighbours of a keyword match are not answers to the question) and `2` for a `selector`.
1437
1597
  - `max_tokens` — budget, floored at 50, default 4000. A node that doesn't fit is skipped and the next tried (`trace.truncated_by_budget` counts the skips); only when nothing fits does the first node go through oversized.
1438
1598
  - 400 `prompt or selector is required` if both missing.
1439
1599
  - Each node carries a `url` deep link, and its context block a matching `_source: <url>_` line, so answers can cite sources. Built from `PUBLIC_BASE_URL`, falling back to the request origin; omitted when neither resolves.
@@ -1852,6 +2012,8 @@ Model Context Protocol endpoints — JSON-RPC over HTTP. Not exercised manually;
1852
2012
 
1853
2013
  **Denials are JSON-RPC.** Gate rejections on the MCP transport path return a JSON-RPC error envelope — `{ "jsonrpc": "2.0", "id": <echoed>, "error": { "code": -32000, "message": "<what failed — actionable reason>" } }` — instead of plain `{error}` JSON, so external clients (PromptOwl, Claude Desktop, Cursor) surface *why* a connection failed: key scoped to a different nest, server suspended, license required, or nest not found / no access.
1854
2014
 
2015
+ **Every tool takes `client`.** The `{ agent, session_id, …custom }` block described in [Caller attribution](#caller-attribution-client) is advertised on every tool's input schema, reads included — a write that seals a version records it there (`context_versions` serves it back), and every call records it on the activity trace. It is never authenticated and never used to authorize. Omitted, the server fills what the connection reports (the API key's label, an `Mcp-Session-Id` header) under anything the caller sent, per key.
2016
+
1855
2017
  **Titles are unique on this surface.** `context_get`, `context_update`, `context_versions`, `context_delete`, and `context_move` each resolve a document by title or by id (an id wins when both are sent, and an ambiguous title refuses rather than guessing). Title addressing is what makes uniqueness matter, so `context_create` refuses a title already used anywhere in the nest — including in another folder — and answers with the existing node's id plus what to do instead (`context_update` to change it, `context_move` to refile it). A duplicate would otherwise be unreachable by every tool here. REST `POST /nodes` keeps the looser rule (same title in a different folder is fine); both surfaces refuse a create whose id is already taken.
1856
2018
 
1857
2019
  **`context_search` is the REST search.** The tool runs the same index-backed match as `GET /nests/:id/search` — terms against the cached discovery set (title, body, type, tags), title hits first — and gates the *hits* with the same `filterAccessible` rule (public readers: approved-only; governed nests: steward coverage or a share grant), so the two surfaces return the same documents in the same order. It takes an optional `limit` (default 20, max 200) and answers `{ results, count }`: `count` is everything found, `results` the first `limit` hits, each with its LLM-visible body (approved version under stewardship; a hit with no approved version yet is listed but carries no body). It used to resolve every document's gated body *before* matching — one permission lookup plus one version reconstruction per node — which is what timed out `ctx search --vault <remote>` on large governed nests.
@@ -2053,6 +2215,7 @@ For any `/nests/:nestId/...` request, the middleware computes:
2053
2215
  - Required level by path:
2054
2216
  - `/collaborators`, `/visibility` → `admin`
2055
2217
  - `/reader-mode` → `admin` at the middleware, then the handler hard-gates on `isServerAdminUserId` (superadmin only)
2218
+ - `/branding` → `admin` at the middleware (owner, admin collaborator, or server admin), re-checked in the handler; `hide_powered_by: true` additionally requires the server admin
2056
2219
  - Stewardship action paths (`/approve`, `/reject`, `/submit-review`, `/cancel-review`) → `read` (handler enforces fine-grained steward gating)
2057
2220
  - Other non-GET → `write`
2058
2221
  - GET → `read`