@promptowl/contextnest-community 1.23.0 → 1.25.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.
- package/API.md +2195 -0
- package/CONFIGURATION.md +8 -2
- package/README.md +62 -10
- package/STEWARDSHIP.md +253 -0
- package/dist/{chunk-2YPY2HGZ.js → chunk-2KHM5C7V.js} +40 -5
- package/dist/{chunk-XYD6V2LH.js → chunk-32SZCH36.js} +28 -33
- package/dist/{chunk-QLXC6542.js → chunk-7FROMOZM.js} +69 -27
- package/dist/{chunk-MLQU4I5J.js → chunk-DTLMBF4W.js} +10 -8
- package/dist/{chunk-QWNXWRWO.js → chunk-JDAOA5KP.js} +67 -1
- package/dist/{chunk-UOMW7TT6.js → chunk-KPGNZLZL.js} +2 -2
- package/dist/{chunk-YVMSM7LS.js → chunk-LHWOJPFE.js} +7 -0
- package/dist/{chunk-5CVZMHHB.js → chunk-TI7HMP64.js} +99 -12
- package/dist/{client-LWDMNKX3.js → client-D44ZHDTJ.js} +1 -1
- package/dist/{engine-QFF2IA3G.js → engine-TER6FDBP.js} +3 -3
- package/dist/{external-edit-service-F7D3VSA2.js → external-edit-service-CAGENHDJ.js} +4 -4
- package/dist/{grants-service-TBGTCO5K.js → grants-service-2OTFS7EH.js} +3 -3
- package/dist/index.js +1120 -331
- package/dist/{migrations.postgres-APCVSYUE.js → migrations.postgres-23VJJJJX.js} +56 -5
- package/dist/{review-service-A7PNLWG5.js → review-service-4G2XUFS6.js} +7 -7
- package/dist/{stewardship-service-32UHIRQ5.js → stewardship-service-S22V2JKG.js} +6 -4
- package/dist/{version-service-RJ6CWZ3O.js → version-service-X4FAG2OZ.js} +4 -4
- package/dist/web3/assets/ActivityTracePage-D2iIbEph.js +1 -0
- package/dist/web3/assets/AgentDocsPage-Dkhgb1hk.js +1 -0
- package/dist/web3/assets/CollaboratorManager-Bo81D--Q.js +1 -0
- package/dist/web3/assets/CollaboratorsTab-DRCitQuX.js +1 -0
- package/dist/web3/assets/DocumentEditor-DV2WHYbj.js +36 -0
- package/dist/web3/assets/DocumentsTab-BUxBCN8o.js +6 -0
- package/dist/web3/assets/ExternalEditsTab-kIBimZyE.js +1 -0
- package/dist/web3/assets/MarkdownEditor-CsVusP2d.css +1 -0
- package/dist/web3/assets/MarkdownEditor-Dfk5uIE5.js +648 -0
- package/dist/web3/assets/NestPageHeader-yOK-OIYZ.js +1 -0
- package/dist/web3/assets/NestView-DwIagxwJ.js +68 -0
- package/dist/web3/assets/OverviewTab-DFOFMVyE.js +1 -0
- package/dist/web3/assets/PersonCombobox-D350WlrJ.js +1 -0
- package/dist/web3/assets/ReasonDialog-CN3ECAnQ.js +1 -0
- package/dist/web3/assets/ReviewActions-BXDTDPp1.js +6 -0
- package/dist/web3/assets/ReviewTab-CZewAYiz.js +1 -0
- package/dist/web3/assets/StewardsTab-AagNWcWS.js +1 -0
- package/dist/web3/assets/SubmitForReviewModal-BOZJxwRN.js +1 -0
- package/dist/web3/assets/alert-dialog-4SiLKlz8.js +7 -0
- package/dist/web3/assets/arrow-left-BAgIocLn.js +6 -0
- package/dist/web3/assets/backlinks-Cb8Uf8mw.js +24 -0
- package/dist/web3/assets/card-UDyMRcps.js +1 -0
- package/dist/web3/assets/chevron-left-XR4ReJ9Z.js +6 -0
- package/dist/web3/assets/circle-check-CSkZUEFK.js +6 -0
- package/dist/web3/assets/circle-x-in2o3aHU.js +6 -0
- package/dist/web3/assets/client-attribution-BraarYaP.js +1 -0
- package/dist/web3/assets/code-xml-pPQgdctI.js +6 -0
- package/dist/web3/assets/corner-down-right-CuxZWscX.js +6 -0
- package/dist/web3/assets/count-skeleton-DBWCDhRy.js +1 -0
- package/dist/web3/assets/dates-BCxbm4_q.js +1 -0
- package/dist/web3/assets/earth-CXOcThqn.js +6 -0
- package/dist/web3/assets/file-exclamation-point-CSphs1VD.js +6 -0
- package/dist/web3/assets/folder-input-BAwK9tFL.js +11 -0
- package/dist/web3/assets/folder-target-CUSWqImF.js +1 -0
- package/dist/web3/assets/index-A0_ymcqL.js +29 -0
- package/dist/web3/assets/index-B5hLclEq.js +389 -0
- package/dist/web3/assets/index-DrUAtoQM.css +1 -0
- package/dist/web3/assets/page-BHD4beun.js +1 -0
- package/dist/web3/assets/page-BSYUXYmO.js +11 -0
- package/dist/web3/assets/page-BYc2DIux.js +45 -0
- package/dist/web3/assets/page-BfibLg8e.js +1 -0
- package/dist/web3/assets/page-CGe8MtUu.js +9 -0
- package/dist/web3/assets/page-CIgPJmG6.js +2 -0
- package/dist/web3/assets/page-CUjuSs5Q.js +24 -0
- package/dist/web3/assets/page-Ck31nx9b.js +1 -0
- package/dist/web3/assets/page-Cpdj_11Y.js +1 -0
- package/dist/web3/assets/page-D0sN3GO6.js +1 -0
- package/dist/web3/assets/page-DEpH91X2.js +1 -0
- package/dist/web3/assets/page-l8-B7CGt.js +1 -0
- package/dist/web3/assets/page-mnSHmk6A.js +6 -0
- package/dist/web3/assets/page-title-BkqlUA0j.js +1 -0
- package/dist/web3/assets/page-xzYqkBUP.js +16 -0
- package/dist/web3/assets/play-B8xE9j5f.js +6 -0
- package/dist/web3/assets/refresh-cw-CsCtmWwX.js +6 -0
- package/dist/web3/assets/scroll-area-CbcH6yrZ.js +1 -0
- package/dist/web3/assets/scroll-area-dRWncRqa.css +1 -0
- package/dist/web3/assets/select-DrL0mpRN.js +6 -0
- package/dist/web3/assets/send-CvAgOUPG.js +6 -0
- package/dist/web3/assets/settings-EY4A_DYv.js +6 -0
- package/dist/web3/assets/share-2-vGmZUl90.js +6 -0
- package/dist/web3/assets/tag-Br_y1f00.js +11 -0
- package/dist/web3/assets/trash-2-B4hRXo-p.js +6 -0
- package/dist/web3/assets/triangle-alert-CsYGyfGZ.js +6 -0
- package/dist/web3/assets/user-plus-4CTYEkd3.js +6 -0
- package/dist/web3/assets/x-qxt1WrUi.js +6 -0
- package/dist/web3/assets/zap-BcsKR0ef.js +16 -0
- package/dist/web3/index.html +2 -2
- package/package.json +5 -3
- package/dist/web3/assets/index-BJ-LRNis.js +0 -1382
- package/dist/web3/assets/index-DPMEt-A_.css +0 -1
package/CONFIGURATION.md
CHANGED
|
@@ -60,7 +60,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
60
60
|
| `PROMPTOWL_SIGN_IN_GATE` | `open` | Restrict "Sign in with PromptOwl". `open` = anyone may; `admin-only` = only the license owner (admin) may, everyone else uses email/password (admin opens the login page with `?admin=1`); `disabled` = nobody may. Enforced server-side at `POST /auth/promptowl` and surfaced on the health endpoint. Unknown values fall back to `open`. |
|
|
61
61
|
| `MANUAL_SIGN_IN` | `open` | Email + password sign-in mode. `open` = anyone may log in and self-register a new account; `invite-only` = existing/invited users may log in but brand-new self-registration returns `403` (the admin provisions accounts via invite/share/steward and shares the password — there is no self-service "set password", which would be account takeover without email verification); `disabled` = no email/password sign-in at all (`POST /auth/login` and `POST /auth/register` return `403`). Independent of `PROMPTOWL_SIGN_IN_GATE`, so the two methods are controlled separately (e.g. `invite-only` manual + `admin-only` PromptOwl). Also settable from Settings → General. The server refuses `disabled` while PromptOwl sign-in is also `disabled` (that would leave no way to log in). Unknown values fall back to `open`. |
|
|
62
62
|
| `OFFICIAL_COMMUNITY_SSO_SECRET` | `""` | **Legacy — official deployment only, leave unset on self-hosted.** Shared HMAC secret enabling the one-click "Open Community" SSO auto-login from PromptOwl. Must exactly match the same-named var on PromptOwl. Superseded by the DB-backed **Settings → Community sites** list (`/admin/community-sites`), which supports multiple official sites each with their own name/url/secret/active flag — this env var still works as an implicit extra site for backward compatibility. When no site is configured (env var unset and no DB rows), `GET /auth/sso` returns `404` and the feature is disabled; self-hosted users keep using the manual device-code flow. See `API.md → Community sites`. |
|
|
63
|
-
| `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`). Two uses. (1) Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another — whenever a ticket-signing secret is set (`OFFICIAL_COMMUNITY_SSO_SECRET` on the official deployment, `MCP_SIGNING_SECRET` on a self-hosted one); with neither set no ticket verifies at all, so the audience check is moot. (2) **Set this whenever `OIDC_ENABLED=true`.** It's the base for the OIDC `redirect_uri` sent to your IdP and replayed at token exchange. Unset, that URI is derived from the incoming request's `Host` header — fine when your proxy overwrites `Host` with a trusted value, but a proxy that passes an attacker-supplied `Host` through would feed it straight into the redirect URI. Setting this pins the value regardless of what the proxy forwards. (Your IdP's own redirect-URI allowlist is a second line of defence, not a substitute.) (3) **Set this wherever email- or invite-gated publish links are used.** The magic link mailed by `POST /p/:slug/gate` is built from it; unset, it falls back to the request origin, so a proxy forwarding an attacker-supplied `Host` would send the recipient a one-time access token pointing at the attacker's domain — the classic reset-link poisoning. (4) **Set this wherever `POST /nests/:id/context` citations reach users.** The `url` on each node and the `_source:` line in each context block are built from it; unset, they fall back to the request origin, so a proxy forwarding an untrusted `Host` would put an attacker-influenced URL in front of both the model and the reader as a trustworthy citation. |
|
|
63
|
+
| `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`). Two uses. (1) Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another — whenever a ticket-signing secret is set (`OFFICIAL_COMMUNITY_SSO_SECRET` on the official deployment, `MCP_SIGNING_SECRET` on a self-hosted one); with neither set no ticket verifies at all, so the audience check is moot. (2) **Set this whenever `OIDC_ENABLED=true`.** It's the base for the OIDC `redirect_uri` sent to your IdP and replayed at token exchange. Unset, that URI is derived from the incoming request's `Host` header — fine when your proxy overwrites `Host` with a trusted value, but a proxy that passes an attacker-supplied `Host` through would feed it straight into the redirect URI. Setting this pins the value regardless of what the proxy forwards. (Your IdP's own redirect-URI allowlist is a second line of defence, not a substitute.) (3) **Set this wherever email- or invite-gated publish links are used.** The magic link mailed by `POST /p/:slug/gate` is built from it; unset, it falls back to the request origin, so a proxy forwarding an attacker-supplied `Host` would send the recipient a one-time access token pointing at the attacker's domain — the classic reset-link poisoning. (4) **Set this wherever `POST /nests/:id/context` citations reach users.** The `url` on each node and the `_source:` line in each context block are built from it; unset, they fall back to the request origin, so a proxy forwarding an untrusted `Host` would put an attacker-influenced URL in front of both the model and the reader as a trustworthy citation. (5) It is rendered into the copy-paste setup snippets on **Settings → Connecting agents** — a `bash` block and quoted strings in `config.toml` / `config.yaml` / `mcp.json`. Because those are pasted verbatim into other people's terminals, `PATCH /admin/settings` refuses a value that is not a plain `http(s)` URL, or that contains whitespace, quotes or shell characters (`" ' ` \ $ ; | & < > ( ) { }`). A value stored before that check is not rewritten, so re-save it if the panel shows it oddly. |
|
|
64
64
|
| `OIDC_ENABLED` | `false` | Turn on generic OIDC single sign-on (`GET /auth/oidc/login` / `GET /auth/oidc/callback`). Requires `OIDC_ISSUER`, `OIDC_CLIENT_ID`, and `OIDC_CLIENT_SECRET` — the login-page button only appears once all three are set. Also editable from Settings → Single sign-on. See [Single sign-on (OIDC)](#single-sign-on-oidc). |
|
|
65
65
|
| `OIDC_ISSUER` | `""` | OIDC issuer URL, e.g. `https://login.microsoftonline.com/<tenant>/v2.0` (Microsoft Entra ID) or `https://accounts.google.com` (Google). **`https://` only** — a non-https value is rejected with a warning and SSO stays off. Must serve `<issuer>/.well-known/openid-configuration`. |
|
|
66
66
|
| `OIDC_CLIENT_ID` | `""` | Application (client) ID from your IdP app registration. |
|
|
@@ -68,6 +68,7 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
68
68
|
| `OIDC_ALLOWED_DOMAINS` | `""` (any) | Comma-separated email-domain allowlist, e.g. `acme.com, contractors.acme.com`. When set, only accounts whose asserted email is on a listed domain may sign in (others bounce with `domain_not_allowed`). Empty allows any domain the IdP asserts. |
|
|
69
69
|
| `OIDC_AUTO_PROVISION` | `true` | Create a user automatically on first successful OIDC sign-in (display name from the `name` claim). Set `false` to allow only pre-existing (invited/registered) users — unknown emails bounce with `not_invited`. |
|
|
70
70
|
| `PEOPLE_SUGGEST_ALL_USERS` | `false` | Widen the add-a-person suggestions (`GET /people/suggest`, behind every "add a person" field) and the `@mention` pickers from *people the caller already shares a nest or team with* to **every registered account on this server**. **Leave this off on a server that hosts more than one organisation** — the PromptOwl-hosted deployment does, and turning it on there would offer one customer's staff another customer's email addresses. On a single-company self-hosted server the whole directory is the useful answer, which is what this is for. Suggestions never gate the invite either way: an address that appears in no list is still valid to type. In the `@` pickers the widening is an invite: a write+ author mentioning someone off the nest adds them as a `read` collaborator before notifying (a read-only author's mention of an outsider reaches nobody). A server admin always sees every account (they administer them). Also editable from **Settings → General → People directory**. |
|
|
71
|
+
| `SHARED_DEPLOYMENT` | `false` | This server hosts **several unrelated organizations** (the PromptOwl-hosted deployment) rather than one company. A nest's `org` visibility means *every authenticated account on this deployment* — on a single-company self-hosted server that is the organization, so the tier flips in one click; on a shared server it is every customer. With this on: `PATCH /nests/:id/visibility` to `org` requires `acknowledge_public: true` exactly as `public` always does (enforced by the server, not only the UI), and the UI's visibility picker confirms before *Organization* and words both tiers to say who they really reach. Reported on `GET /health` as `shared_deployment`. Also editable from **Settings → General → This server hosts several organizations**. **Turning it on guards future changes only** — nests already at `org` stay readable by every account until their admins reconfirm or narrow them. On an already-populated server, audit them first: the dashboard's *Organization* scope lists every one, or `SELECT id, name, user_id FROM nests WHERE visibility = 'org'`. |
|
|
71
72
|
| `OIDC_DEPARTMENT_TAGGING` | `false` | Auto-tag newly **created** documents with the creator's directory department: `dept:<slugified-department>` (lowercase, spaces → dashes, e.g. `dept:customer-success`) is appended to the document's tags, deduped against user-supplied tags. Applies on create only — never on update, never retroactively — and a user without a stored department is a silent no-op. The department is captured from the OIDC `department` ID-token claim on every SSO login (a login without the claim clears it, so directory moves propagate), so this is only meaningful when your IdP emits that claim — see [Department auto-tagging](#department-auto-tagging). Also editable from Settings → Single sign-on. |
|
|
72
73
|
| `SSO_TOKEN_EXCHANGE_ENABLED` | `false` | Master switch for `POST /auth/token-exchange` — an external agent exchanges an IdP ID token for a short-lived MCP bearer. Also needs `MCP_SIGNING_SECRET` and at least one provider; otherwise the endpoint returns `404`. Security-critical — see [Agent SSO (token exchange)](#agent-sso-token-exchange). |
|
|
73
74
|
| `MCP_SIGNING_SECRET` | `""` | HMAC secret **this** server signs its minted MCP bearers with (distinct from `OFFICIAL_COMMUNITY_SSO_SECRET`, so self-hosted deployments can issue their own). Long random string. `/index` and `/mcp` accept a ticket signed with either secret. Write-only on the Settings API. **Clearing it is the revocation switch** — turning `SSO_TOKEN_EXCHANGE_ENABLED` off stops minting but already-minted bearers stay valid until they expire. |
|
|
@@ -80,10 +81,15 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
80
81
|
| `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
82
|
| `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
83
|
| `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. |
|
|
84
|
+
| `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. |
|
|
85
|
+
| `CONTEXTNEST_AGENT` | `""` | Overrides the `agent` this server derives for caller attribution (`client`, spec §9.4) when a call sends none of its own. Without it the name comes from the MCP `initialize` handshake's `clientInfo.name` when the connection reports one, else the label of the API key the call authenticated with. A caller's own `client.agent` always wins. |
|
|
86
|
+
| `CONTEXTNEST_SESSION_ID` | `""` | Same, for `session_id` — otherwise the MCP transport's session id, or an `Mcp-Session-Id` request header when the client sends one. Left absent when nothing reports one: the `/mcp` endpoints are stateless and issue no session, and a placeholder would be indistinguishable from a real id. |
|
|
87
|
+
| `CONTEXTNEST_NO_ATTRIBUTION` | `""` | Set to `1` to derive nothing at all. What this server derives lands in an append-only version history, so an operator who does not want an agent's name recorded there permanently needs to say so before the first write. A `client` the caller sent explicitly is still recorded — that is the caller's own record to make. |
|
|
83
88
|
| `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
89
|
| `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
|
-
| `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
|
|
90
|
+
| `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, and the PDF upload route (`POST /nests/:id/nodes/pdf`) up to `PDF_MAX_MB`. |
|
|
86
91
|
| `VIDEO_MAX_MB` | `30` | Max size (in MB) of a video uploaded into a doc. Default `30` keeps it under Cloud Run's ~32 MiB HTTP/1 request-body limit, so an oversized video is rejected with a clear message instead of a bare `413` from the platform. Raise only where the deployment can actually accept larger request bodies (not behind Cloud Run, or on HTTP/2 / direct-to-bucket upload). Images are fixed at 10 MB. |
|
|
92
|
+
| `PDF_MAX_MB` | `25` | Max size (in MB) of a PDF uploaded as a `pdf` document (`POST /nests/:id/nodes/pdf`). A larger file is refused with `413` and nothing is written. Default `25` keeps the request under Cloud Run's ~32 MiB HTTP/1 limit; raise it only where the deployment accepts larger request bodies. The PDF route is exempt from `MAX_BODY_BYTES` up to this cap. |
|
|
87
93
|
| `LOGO_URL` | _(unset)_ | Custom logo shown in the UI header + login screen. Must start with `https://`, `http://`, or `data:image/` — other schemes (`file://`, relative, `javascript:`) are rejected with a warning and the bundled icon is used. |
|
|
88
94
|
| `PROMPTOWL_TEAMS_ENABLED` | _(unset — off)_ | Lets users who signed in with PromptOwl import their PromptOwl teams as local teams. Off by default; set `true` to enable, or toggle from Settings → Advanced. When off, `GET/POST /teams/promptowl` return `404` and the PromptOwl Teams panel is hidden. Independent of `PROMPTOWL_SIGN_IN_GATE`. |
|
|
89
95
|
| `TYPE_ARTIFACT_ENABLED` | `true` | Set `false` to disable creation of **artifact** nodes server-wide (existing artifact nodes stay readable — never data loss). Runnable types (agent/skill/tool) are gated by `FEATURE_WORKFLOW_PLANE`, not here. Also editable from Settings (`/admin/settings`). |
|
package/README.md
CHANGED
|
@@ -8,17 +8,19 @@
|
|
|
8
8
|
|
|
9
9
|
## What it is
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -98,13 +134,21 @@ For redistribution, hosted-service, OEM, or regulated-industry licensing, contac
|
|
|
98
134
|
| Custom logo / branding | ✅ | ✅ |
|
|
99
135
|
| Admin password reset + user removal (in-platform) | ✅ | ✅ |
|
|
100
136
|
| Wiki backlinks, outline, hover-preview, link health | ✅ | ✅ |
|
|
101
|
-
| Rich editor — tables, callouts, toggles, code highlight, find/replace, image & video upload | ✅ | ✅ |
|
|
137
|
+
| Rich editor — tables, callouts, toggles, code highlight, find/replace, image & video upload, YouTube / Vimeo embeds | ✅ | ✅ |
|
|
102
138
|
| Folder organization — nested folders, move documents, lazy folder tree | ✅ | ✅ |
|
|
103
139
|
| Scales to large vaults — nest listings served from a document index, not a disk crawl | ✅ | ✅ |
|
|
104
140
|
| Steward version revert | ✅ | ✅ |
|
|
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,253 @@
|
|
|
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
|
+
## Creators review their own documents (`creator_is_reviewer`)
|
|
94
|
+
|
|
95
|
+
A per-nest flag (**off** by default; nest Settings → "Creators review their own documents") for the GitHub posture: anyone with write access creates documents, and the person who created a document is the one who approves changes to it.
|
|
96
|
+
|
|
97
|
+
When it's on, creating a document in a governed nest does two things:
|
|
98
|
+
|
|
99
|
+
1. **The author is stewarded as a document-scope `reviewer`** of that document — exactly the row an admin would otherwise add by hand (`POST /nests/:id/stewards` with `scope: "document"`, or the `documents:` section of `stewards.yaml`). The nest owner is skipped; they already approve everything.
|
|
100
|
+
2. **The first version publishes immediately.** The author is the document's reviewer, so nobody else needs to bless v1. This holds for the owner too: with this on, the owner's own new documents publish at v1 whether or not `allow_self_approve` is on — the two settings are independent, and this one only ever touches v1.
|
|
101
|
+
|
|
102
|
+
The row is written **before** the publish decision, so v1 only publishes when the author really can approve the next change. If a document-scope row for the author already exists on that id with a lesser role (an admin stewarded the id ahead of time), it is left alone and v1 lands as a draft instead. If the row can't be written, the create fails with nothing written.
|
|
103
|
+
|
|
104
|
+
Everything after v1 is the ordinary review cycle, and separation of duties is untouched:
|
|
105
|
+
|
|
106
|
+
| | Who approves |
|
|
107
|
+
|---|---|
|
|
108
|
+
| A teammate edits the document | The creator (plus any tag- or nest-level reviewer, and admins) |
|
|
109
|
+
| The creator edits their own document | Another reviewer — the creator can't approve their own submission |
|
|
110
|
+
|
|
111
|
+
**Prime wins.** A prime document still lands as a draft: its creator is made its reviewer, but someone else has to approve the first version.
|
|
112
|
+
|
|
113
|
+
Only **future** creates are affected. Turning it on doesn't steward anyone on existing documents, and turning it off leaves every steward row it created in place — remove them from the Stewards tab like any other. Every create surface participates (the editor, the REST and MCP APIs, and `ctx push` — the pusher authored what they pushed); the derived annotations document is the one exception, since nobody authored it.
|
|
114
|
+
|
|
115
|
+
Like prime, this only applies to **governed** nests. With stewardship off there is nothing to review, so the flag is inert until you turn stewardship on.
|
|
116
|
+
|
|
117
|
+
## Who sees what
|
|
118
|
+
|
|
119
|
+
Governance gates two things: who can **approve**, and who can **read**.
|
|
120
|
+
|
|
121
|
+
- **Approve gate**: server-side, always on when stewardship is enabled. Non-stewards can't approve.
|
|
122
|
+
- **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.
|
|
123
|
+
|
|
124
|
+
In key mode, when stewardship is enabled on a nest:
|
|
125
|
+
- Node list / single-read / search / query responses are filtered to docs the caller can access
|
|
126
|
+
- A non-steward gets an empty list or a 403 on the node they asked for
|
|
127
|
+
- The nest **owner** and any resolved **steward (any role)** can read; **super admins** bypass the check
|
|
128
|
+
|
|
129
|
+
## Server super-admins
|
|
130
|
+
|
|
131
|
+
Super-admins administer **every** nest on the server without being added per-nest. The effective set is the union of three sources:
|
|
132
|
+
|
|
133
|
+
- **license** — the PromptOwl account that owns the installed license (always; never revocable)
|
|
134
|
+
- **config** — emails in `access.yaml: super_admins` (the bootstrap; edit the file + restart to change)
|
|
135
|
+
- **granted** — grants managed from **Teammates → Superadmins** in the UI, or `GET/POST /admin/super-admins` (see `API.md`)
|
|
136
|
+
|
|
137
|
+
A super-admin can:
|
|
138
|
+
|
|
139
|
+
- Read every document (bypasses the read gate, even with stewardship enabled)
|
|
140
|
+
- Change a nest's visibility (private / org / public)
|
|
141
|
+
- Add, remove, and re-role collaborators
|
|
142
|
+
- Add, remove, and re-role stewards
|
|
143
|
+
- Approve and reject pending versions in any nest's review queue
|
|
144
|
+
- Manage the official community sites trusted for SSO auto-login (**Server settings → Community sites**, or `GET/POST /admin/community-sites` — see `API.md`)
|
|
145
|
+
|
|
146
|
+
What a super-admin **cannot** do (owner-only operations):
|
|
147
|
+
|
|
148
|
+
- Delete a nest
|
|
149
|
+
- Transfer ownership
|
|
150
|
+
|
|
151
|
+
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.
|
|
152
|
+
|
|
153
|
+
## Teams — sharing with a group
|
|
154
|
+
|
|
155
|
+
Instead of adding people one at a time, you can create a **team** — a reusable,
|
|
156
|
+
owner-managed group — and share it onto a nest. Each team member carries a
|
|
157
|
+
**role** set on the membership (not on the share), and that role applies on
|
|
158
|
+
**every** nest the team is shared to:
|
|
159
|
+
|
|
160
|
+
| Member role | Nest access | Governance |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| **viewer** | read | viewer |
|
|
163
|
+
| **editor** | write | editor |
|
|
164
|
+
| **admin** | admin | admin |
|
|
165
|
+
|
|
166
|
+
Key points:
|
|
167
|
+
|
|
168
|
+
- A member's role is uniform across nests — set it once on the team. To vary
|
|
169
|
+
access per nest, use a different team (or a direct collaborator/steward grant).
|
|
170
|
+
- When a user reaches a nest by several paths (a direct collaborator grant **and**
|
|
171
|
+
team membership), the **highest** access wins.
|
|
172
|
+
- Sharing a team whose members carry a governance role (viewer/editor)
|
|
173
|
+
**enables stewardship** on the nest — same effect as assigning an individual
|
|
174
|
+
steward. An `admin`-only team is collaborator-style and doesn't.
|
|
175
|
+
- Removing a member, deleting the team, or unsharing it **revokes access
|
|
176
|
+
immediately** — there are no per-user rows to clean up.
|
|
177
|
+
- Managing a team is gated by your role on it: editor/admin members (and the
|
|
178
|
+
owner) can add people, capped to their own level; admins can also change
|
|
179
|
+
roles and remove members; only the owner can delete the team.
|
|
180
|
+
|
|
181
|
+
### Importing a team from PromptOwl
|
|
182
|
+
|
|
183
|
+
If you already run teams in PromptOwl, the Teams page has a **PromptOwl Teams**
|
|
184
|
+
tab listing your teams there with their members. Each one has a **Sync** button
|
|
185
|
+
that copies it here as a normal team owned by you, instead of you retyping every
|
|
186
|
+
member. Members without an account here become invited placeholders, exactly as
|
|
187
|
+
if you'd added them by hand.
|
|
188
|
+
|
|
189
|
+
**The tab only appears if you signed in with PromptOwl** — that sign-in is what
|
|
190
|
+
gives this server permission to read your teams. Signing in another way (SSO,
|
|
191
|
+
your identity provider, or email and password) means no PromptOwl identity to
|
|
192
|
+
read teams for, so the tab isn't offered.
|
|
193
|
+
|
|
194
|
+
**You can import a team you're an owner or editor of in PromptOwl.** Teams you're
|
|
195
|
+
view-only on are listed for reference but can't be imported: the copy would be
|
|
196
|
+
yours to control here — rename it, rewrite its roster, share it onto a nest —
|
|
197
|
+
and that's more than PromptOwl lets you do with that team. Ask one of its owners
|
|
198
|
+
or editors to import it instead.
|
|
199
|
+
|
|
200
|
+
Already-imported teams are marked **Synced** in that tab, and show a **PromptOwl**
|
|
201
|
+
badge in the ContextNest Teams tab. Two things worth knowing before you sync
|
|
202
|
+
one again:
|
|
203
|
+
|
|
204
|
+
- **PromptOwl is the source of truth on every sync.** The roster is reconciled to
|
|
205
|
+
match: roles are reset, and anyone no longer in the PromptOwl team is removed —
|
|
206
|
+
including people you added to this team by hand here. If you need a group that
|
|
207
|
+
differs from PromptOwl, make it a separate team rather than editing an
|
|
208
|
+
imported one.
|
|
209
|
+
- **Your PromptOwl access isn't stored on this server.** The connection lives in
|
|
210
|
+
a cookie in your own browser and ends when your session does — this server's
|
|
211
|
+
database never holds anything that can reach your PromptOwl account, so a
|
|
212
|
+
stolen backup of it exposes nothing of yours. The connection is also limited
|
|
213
|
+
to reading your profile and your teams: it cannot chat as you or spend your
|
|
214
|
+
credits. Signing out ends it, and revoking the device in PromptOwl's account
|
|
215
|
+
settings ends it immediately from the other side.
|
|
216
|
+
|
|
217
|
+
PromptOwl's `User` role — its default membership — maps to **viewer** here, as
|
|
218
|
+
does any role this version doesn't recognize; `Owner` maps to admin, and
|
|
219
|
+
`Editor`/`Viewer` map across as themselves.
|
|
220
|
+
|
|
221
|
+
Members see the teams they belong to on the **Teams** page and any team-shared
|
|
222
|
+
nest on their dashboard. Manage teams there (create, add/remove members with a
|
|
223
|
+
role); share them from a nest's sharing panel. See `API.md` §3a for the endpoints.
|
|
224
|
+
|
|
225
|
+
## Picking a scope when you assign a steward
|
|
226
|
+
|
|
227
|
+
Rules of thumb:
|
|
228
|
+
|
|
229
|
+
- **Nest scope** — use sparingly. "This person reviews everything in this nest."
|
|
230
|
+
- **Tag scope** — the most common. "Legal reviews anything with `#legal`. Security reviews `#auth` and `#pii`."
|
|
231
|
+
- **Document scope** — override. "This specific doc has a dedicated reviewer regardless of its tags."
|
|
232
|
+
|
|
233
|
+
You can stack them: a doc tagged `#legal` can have a doc-level steward that overrides the tag steward. Priority order resolves ties.
|
|
234
|
+
|
|
235
|
+
## `stewards.yaml` (optional)
|
|
236
|
+
|
|
237
|
+
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:
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
version: 1
|
|
241
|
+
nest:
|
|
242
|
+
- email: governance-lead@acme.com
|
|
243
|
+
role: reviewer
|
|
244
|
+
tags:
|
|
245
|
+
"#legal":
|
|
246
|
+
- email: legal@acme.com
|
|
247
|
+
role: reviewer
|
|
248
|
+
"#security":
|
|
249
|
+
- email: security-team@acme.com
|
|
250
|
+
role: reviewer
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
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.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildTitleMap,
|
|
3
|
+
canApproveWith,
|
|
3
4
|
canEditWith,
|
|
4
5
|
canViewWith,
|
|
5
6
|
collabPermToRole,
|
|
@@ -8,19 +9,20 @@ import {
|
|
|
8
9
|
nestAllowsSelfApprove,
|
|
9
10
|
nestName,
|
|
10
11
|
primaryRole,
|
|
12
|
+
resolveNestAccess,
|
|
11
13
|
resolveNestPermission,
|
|
12
14
|
resolveTeamRolesForUser,
|
|
13
15
|
sendEmailToRecipient
|
|
14
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-7FROMOZM.js";
|
|
15
17
|
import {
|
|
16
18
|
ConflictError,
|
|
17
19
|
ValidationError
|
|
18
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-LHWOJPFE.js";
|
|
19
21
|
import {
|
|
20
22
|
config,
|
|
21
23
|
getDb,
|
|
22
24
|
isEmailish
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-JDAOA5KP.js";
|
|
24
26
|
|
|
25
27
|
// src/governance/stewardship-service.ts
|
|
26
28
|
import { v4 as uuid } from "uuid";
|
|
@@ -282,6 +284,38 @@ async function createStewardRecord(params) {
|
|
|
282
284
|
}
|
|
283
285
|
return results;
|
|
284
286
|
}
|
|
287
|
+
async function stewardDocumentCreator(nestId, nodeId, userEmail) {
|
|
288
|
+
const email = userEmail.trim().toLowerCase();
|
|
289
|
+
if (!email) return { canApprove: false };
|
|
290
|
+
const ownerEmail = (await getNestOwnerEmail(nestId) || "").toLowerCase();
|
|
291
|
+
if (email === ownerEmail) return { canApprove: true };
|
|
292
|
+
const row = {
|
|
293
|
+
nestId,
|
|
294
|
+
scope: "document",
|
|
295
|
+
nodePattern: nodeId,
|
|
296
|
+
userEmail: email,
|
|
297
|
+
userId: await userIdForEmail(email),
|
|
298
|
+
role: "reviewer",
|
|
299
|
+
assignedBy: email,
|
|
300
|
+
assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
301
|
+
isActive: true
|
|
302
|
+
};
|
|
303
|
+
try {
|
|
304
|
+
const created = await assignSteward(row);
|
|
305
|
+
return { canApprove: true, insertedId: created.id };
|
|
306
|
+
} catch (err) {
|
|
307
|
+
if (!isUniqueViolation(err)) throw err;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
canApprove: canApproveWith(
|
|
311
|
+
await resolveUserRoles(nestId, email, { nodeId })
|
|
312
|
+
)
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function isUniqueViolation(err) {
|
|
316
|
+
const code = err?.code;
|
|
317
|
+
return code === "SQLITE_CONSTRAINT_UNIQUE" || code === "23505";
|
|
318
|
+
}
|
|
285
319
|
async function resolveStewardsForNode(nestId, nodeId) {
|
|
286
320
|
return (await resolve(nestId, nodeId)).stewards;
|
|
287
321
|
}
|
|
@@ -441,8 +475,8 @@ async function resolveUserRoles(nestId, userEmail, opts) {
|
|
|
441
475
|
}
|
|
442
476
|
async function canManageStewards(nestId, userId) {
|
|
443
477
|
if (config.AUTH_MODE === "open") return true;
|
|
444
|
-
const
|
|
445
|
-
return
|
|
478
|
+
const { rawPermission } = await resolveNestAccess(nestId, userId);
|
|
479
|
+
return rawPermission === "owner" || rawPermission === "admin";
|
|
446
480
|
}
|
|
447
481
|
async function canCreateInNest(nestId, userEmail) {
|
|
448
482
|
if (config.AUTH_MODE === "open" || isSuperAdmin(userEmail)) return true;
|
|
@@ -637,6 +671,7 @@ export {
|
|
|
637
671
|
getStewardsForScope,
|
|
638
672
|
listStewards,
|
|
639
673
|
createStewardRecord,
|
|
674
|
+
stewardDocumentCreator,
|
|
640
675
|
resolveStewardsForNode,
|
|
641
676
|
resolveStewardsWithFallback,
|
|
642
677
|
getStewardRolesForUser,
|
|
@@ -2,13 +2,14 @@ import {
|
|
|
2
2
|
engineCache,
|
|
3
3
|
indexedNodeIds,
|
|
4
4
|
isNestArchived,
|
|
5
|
+
listSuggestionRows,
|
|
5
6
|
markIndexStale,
|
|
6
7
|
resolveNestPath,
|
|
7
|
-
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
syncSuggestionRows
|
|
9
|
+
} from "./chunk-7FROMOZM.js";
|
|
9
10
|
import {
|
|
10
11
|
getDb
|
|
11
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JDAOA5KP.js";
|
|
12
13
|
|
|
13
14
|
// src/governance/external-edit-service.ts
|
|
14
15
|
import { readFile, readdir } from "fs/promises";
|
|
@@ -82,7 +83,7 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
|
82
83
|
if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
|
|
83
84
|
return null;
|
|
84
85
|
}
|
|
85
|
-
const { hasUnsealedDraft } = await import("./version-service-
|
|
86
|
+
const { hasUnsealedDraft } = await import("./version-service-X4FAG2OZ.js");
|
|
86
87
|
if (await hasUnsealedDraft(nestId, documentId)) {
|
|
87
88
|
return null;
|
|
88
89
|
}
|
|
@@ -91,7 +92,7 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
|
91
92
|
const existing = await listSuggestions(storage, documentId);
|
|
92
93
|
const dup = existing.find((s) => s.proposed_hash === drift.actualHash);
|
|
93
94
|
if (dup) {
|
|
94
|
-
await
|
|
95
|
+
await syncSuggestionRows(nestId, documentId, existing);
|
|
95
96
|
return { meta: dup, created: false };
|
|
96
97
|
}
|
|
97
98
|
const result = await stageSuggestion({
|
|
@@ -103,13 +104,13 @@ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
|
|
|
103
104
|
actor,
|
|
104
105
|
docTier: "standard"
|
|
105
106
|
});
|
|
106
|
-
await
|
|
107
|
+
await syncSuggestionRows(nestId, documentId, [...existing, result.meta]);
|
|
107
108
|
return { meta: result.meta, created: true };
|
|
108
109
|
}
|
|
109
110
|
async function refreshSuggestionFlag(nestId, storage, documentId) {
|
|
110
111
|
try {
|
|
111
112
|
const remaining = await listSuggestions(storage, documentId);
|
|
112
|
-
await
|
|
113
|
+
await syncSuggestionRows(nestId, documentId, remaining);
|
|
113
114
|
} catch (err) {
|
|
114
115
|
console.error("[external-edit] flag refresh failed", nestId, documentId, err);
|
|
115
116
|
}
|
|
@@ -170,33 +171,27 @@ async function getPendingChange(nestId, documentId) {
|
|
|
170
171
|
return null;
|
|
171
172
|
}
|
|
172
173
|
async function listNestExternalEdits(nestId) {
|
|
174
|
+
const rows = await listSuggestionRows(nestId);
|
|
175
|
+
if (rows.length === 0) return [];
|
|
173
176
|
const { storage } = await engineCache.get(nestId);
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
177
|
+
const kept = await Promise.all(
|
|
178
|
+
rows.map(
|
|
179
|
+
async (r) => await bodyMatchesLatestVersion(storage, r.node_id, r.proposed_hash) ? null : r
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
return kept.flatMap(
|
|
183
|
+
(r) => r ? [{
|
|
184
|
+
suggestion_id: r.suggestion_id,
|
|
185
|
+
nest_id: nestId,
|
|
186
|
+
document_id: r.node_id,
|
|
187
|
+
source: r.source,
|
|
188
|
+
detected_at: r.detected_at,
|
|
189
|
+
actor: r.actor,
|
|
190
|
+
target_hash: r.target_hash,
|
|
191
|
+
proposed_hash: r.proposed_hash,
|
|
192
|
+
note: r.note ?? void 0
|
|
193
|
+
}] : []
|
|
187
194
|
);
|
|
188
|
-
const entries = lists.flat().map((meta) => ({
|
|
189
|
-
suggestion_id: meta.suggestion_id,
|
|
190
|
-
nest_id: nestId,
|
|
191
|
-
document_id: meta.document_id,
|
|
192
|
-
source: meta.source,
|
|
193
|
-
detected_at: meta.detected_at,
|
|
194
|
-
actor: meta.actor,
|
|
195
|
-
target_hash: meta.target_hash,
|
|
196
|
-
proposed_hash: meta.proposed_hash,
|
|
197
|
-
note: meta.note
|
|
198
|
-
}));
|
|
199
|
-
return entries.sort((a, b) => b.detected_at.localeCompare(a.detected_at));
|
|
200
195
|
}
|
|
201
196
|
async function getExternalEditDetail(nestId, documentId, suggestionId) {
|
|
202
197
|
const { storage } = await engineCache.get(nestId);
|
|
@@ -259,7 +254,7 @@ async function listExternalEditVerdicts(nestId, documentId) {
|
|
|
259
254
|
async function mirrorVersion(input) {
|
|
260
255
|
const { storage } = await engineCache.get(input.nestId);
|
|
261
256
|
const node = await storage.readDocument(input.documentId);
|
|
262
|
-
const { upsertVersion, setApprovedVersion } = await import("./version-service-
|
|
257
|
+
const { upsertVersion, setApprovedVersion } = await import("./version-service-X4FAG2OZ.js");
|
|
263
258
|
await upsertVersion({
|
|
264
259
|
nestId: input.nestId,
|
|
265
260
|
nodeId: input.documentId,
|