@promptowl/contextnest-community 1.8.0 → 1.10.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/CONFIGURATION.md +38 -2
- package/dist/chunk-3JTODC3Y.js +48 -0
- package/dist/chunk-7PQREKSM.js +103 -0
- package/dist/{chunk-PLAHCDQG.js → chunk-BC6KFUZH.js} +442 -25
- package/dist/chunk-FRQJWGN3.js +8 -0
- package/dist/{chunk-PJTAUVD7.js → chunk-KFGZIECB.js} +107 -74
- package/dist/{chunk-XNGOO6RI.js → chunk-UHUU3VAK.js} +4 -2
- package/dist/chunk-XQ46F76G.js +16 -0
- package/dist/chunk-ZM4F7MY7.js +1343 -0
- package/dist/client-VZLX4THW.js +11 -0
- package/dist/email-R7DFS6E5.js +6 -0
- package/dist/grants-service-6H4WUA2F.js +21 -0
- package/dist/index.js +4120 -1049
- package/dist/{migrations.postgres-VCG45LMK.js → migrations.postgres-4XYY3CTF.js} +203 -1
- package/dist/{review-service-G5US3SMG.js → review-service-VLSUT6KK.js} +7 -4
- package/dist/{stewardship-service-ZXAMFTXO.js → stewardship-service-D5PDWXBR.js} +4 -2
- package/dist/{version-service-B3SDOJQE.js → version-service-A2YAKYQW.js} +3 -2
- package/dist/web3/assets/index-B2PkvzmY.js +1007 -0
- package/dist/web3/assets/index-C-tX933G.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +16 -6
- package/dist/chunk-Z3Y46ZGU.js +0 -362
- package/dist/web3/assets/index-Den-5nzY.js +0 -904
- package/dist/web3/assets/index-Jfj-2MHm.css +0 -1
package/CONFIGURATION.md
CHANGED
|
@@ -56,17 +56,28 @@ The server prints a loud warning at startup when `AUTH_MODE=open` is active.
|
|
|
56
56
|
| `DB_SSL_CA` | `""` | Path to a CA certificate (PEM) for verify-ca/verify-full TLS when `DB_SSL=true`. |
|
|
57
57
|
| `AUTH_MODE` | `key` | `key` or `open`. See above. |
|
|
58
58
|
| `PROMPTOWL_API_URL` | `https://app.promptowl.ai` | PromptOwl's API origin — used for device auth, license validation, telemetry. Override for air-gapped or test setups. |
|
|
59
|
-
| `PROMPTOWL_KEY` | `""` | Your PromptOwl Community License key (`pk_...`). Unlicensed instances still run and serve reads, but every write returns `503` until a valid key is installed. Can also be set via the browser License Setup Page, which persists it to `
|
|
59
|
+
| `PROMPTOWL_KEY` | `""` | Your PromptOwl Community License key (`pk_...`). Unlicensed instances still run and serve reads, but every write returns `503` until a valid key is installed. Can also be set via the browser License Setup Page, which persists it to the database (`server_settings` table) so it survives a rebuild and reaches every instance — see [Runtime settings persistence](#runtime-settings-persistence). Setting it here in the deploy environment takes precedence on the next boot. |
|
|
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
|
+
| `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`. |
|
|
61
62
|
| `OFFICIAL_COMMUNITY_SSO_SECRET` | `""` | **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. When unset, `GET /auth/sso` returns `404` and the feature is disabled; self-hosted users keep using the manual device-code flow. |
|
|
62
63
|
| `PUBLIC_BASE_URL` | `""` | This server's canonical external URL (e.g. `https://community.promptowl.ai`). Checked against the SSO ticket's `aud` claim so a ticket minted for this server can't be replayed against another. Only relevant when `OFFICIAL_COMMUNITY_SSO_SECRET` is set; when unset, the audience check is skipped. |
|
|
63
|
-
| `ENV_FILE_PATH` | `$DATA_ROOT/.env` | Path to
|
|
64
|
+
| `ENV_FILE_PATH` | `$DATA_ROOT/.env` | Path to an optional `.env` file the server reads at boot (in addition to `$cwd/.env`). **No longer used for persistence** — the License Setup Page and Settings page now write to the database, not this file (see [Runtime settings persistence](#runtime-settings-persistence)). Kept for operators who bootstrap config from a mounted `.env`. |
|
|
64
65
|
| `TELEMETRY_ENABLED` | `"true"` (set to `"false"` to disable) | Batched, anonymized usage events sent to PromptOwl. Off disables the loop entirely. |
|
|
65
66
|
| `TELEMETRY_INTERVAL_MS` | `3600000` (1 hour) | How often buffered telemetry is flushed to PromptOwl. |
|
|
67
|
+
| `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. |
|
|
66
68
|
| `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). |
|
|
67
69
|
| `MAX_BODY_BYTES` | `10485760` (10 MB) | Reject requests whose `Content-Length` exceeds this. Prevents giant-payload DoS. |
|
|
68
70
|
| `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. |
|
|
71
|
+
| `FEATURE_WORKFLOW_PLANE` | _(unset — off)_ | Enables the workflow plane: typed edges, edge-type registry, governed runs. Optional feature; also toggleable from Settings. |
|
|
72
|
+
| `FEATURE_SUBAGENT_RUNS` | _(unset — off)_ | Lets runs spawn nested sub-agent runs (recursion). Gated separately from the plane; requires `FEATURE_WORKFLOW_PLANE`. Also toggleable from Settings → Advanced (turning the plane off forces this off too). Opens a recursion surface — enable only after reviewing the depth/fan-out caps. |
|
|
73
|
+
| `SUBAGENT_MAX_DEPTH` | `8` | Max sub-agent nesting depth (clamped 1..32) — bounds the call tree's HEIGHT so it stays finite/haltable. |
|
|
74
|
+
| `SUBAGENT_MAX_CHILDREN` | `16` | Max direct children a single run may spawn (clamped 1..128) — bounds the call tree's WIDTH. Together with `SUBAGENT_MAX_DEPTH` this caps total tree size so a runner can't fork-bomb the DB. |
|
|
75
|
+
| `RUN_MAX_STEPS` | `10000` | Max steps a single run may accumulate (clamped 10..100000) — bounds a runaway/hostile runner's step log. |
|
|
69
76
|
| `SLACK_WEBHOOK_URL` | _(unset — connector off)_ | Slack incoming-webhook URL for governance-event notifications (review submitted/approved/rejected, collaborator added). `https://` only — the URL embeds a secret. Also editable from Settings in the UI. |
|
|
77
|
+
| `SMTP_URL` | _(unset — connector off)_ | SMTP connection URL for email notifications (`smtp://` or `smtps://`, credentials inline). Requires `NOTIFY_EMAIL_FROM` and `NOTIFY_EMAIL_TO`. Also editable from Settings. |
|
|
78
|
+
| `NOTIFY_EMAIL_FROM` | _(unset)_ | From address for notification emails. |
|
|
79
|
+
| `NOTIFY_EMAIL_TO` | _(unset)_ | Comma-separated recipients for notification emails. |
|
|
80
|
+
| `NOTIFY_DEBOUNCE_MS` | `15000` | Per-nest buffer window before notifications flush; bursts collapse into one digest message. |
|
|
70
81
|
|
|
71
82
|
---
|
|
72
83
|
|
|
@@ -94,6 +105,31 @@ Postgres backend starts empty.
|
|
|
94
105
|
> shared storage — e.g. mount a GCS bucket via Cloud Storage FUSE, or use a
|
|
95
106
|
> persistent volume — otherwise documents will disappear on redeploy/scale-in.
|
|
96
107
|
|
|
108
|
+
### Runtime settings persistence
|
|
109
|
+
|
|
110
|
+
Settings you change at runtime — everything on the **Settings page** (`/admin/settings`:
|
|
111
|
+
sign-in gate, logo, base URL, upload limit, feature flags, Slack/SMTP connectors)
|
|
112
|
+
plus the **installed license key** — are stored in the database (`server_settings`
|
|
113
|
+
table), **not** in a `.env` file. This is deliberate: on Cloud Run the container
|
|
114
|
+
filesystem is ephemeral, so a file-based value was wiped by every rebuild, and each
|
|
115
|
+
horizontally-scaled instance had its own filesystem, so a change on one never
|
|
116
|
+
reached the others. The database (Cloud SQL in production) is durable **and** shared,
|
|
117
|
+
so a UI change persists across rebuilds and is seen by every instance. Rows are
|
|
118
|
+
loaded into the process environment at boot.
|
|
119
|
+
|
|
120
|
+
**Precedence between the deploy environment and a UI change.** Each stored row
|
|
121
|
+
remembers the deploy-env value in effect when it was written. At boot:
|
|
122
|
+
|
|
123
|
+
- If the deploy env value for that key is **unchanged**, the stored UI change wins
|
|
124
|
+
(your Settings-page edit survives an identical rebuild).
|
|
125
|
+
- If the deploy env value **changed** (you redeployed with a new value for that
|
|
126
|
+
variable), the deploy env wins and the stored row is re-synced to it.
|
|
127
|
+
|
|
128
|
+
So you can always override any setting from the deploy config, while UI edits made
|
|
129
|
+
to keys you don't set in the environment stay put. Clearing a setting in the UI
|
|
130
|
+
writes a tombstone, so a value you removed is not resurrected from the environment
|
|
131
|
+
on the next boot.
|
|
132
|
+
|
|
97
133
|
### Cloud Run + Cloud SQL (PostgreSQL)
|
|
98
134
|
|
|
99
135
|
Attach the Cloud SQL instance to the service (`--add-cloudsql-instances`) so the
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/shared/errors.ts
|
|
2
|
+
var AppError = class extends Error {
|
|
3
|
+
constructor(statusCode, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.statusCode = statusCode;
|
|
6
|
+
this.name = "AppError";
|
|
7
|
+
}
|
|
8
|
+
statusCode;
|
|
9
|
+
};
|
|
10
|
+
var NotFoundError = class extends AppError {
|
|
11
|
+
constructor(message = "Not found") {
|
|
12
|
+
super(404, message);
|
|
13
|
+
this.name = "NotFoundError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var ForbiddenError = class extends AppError {
|
|
17
|
+
constructor(message = "Forbidden") {
|
|
18
|
+
super(403, message);
|
|
19
|
+
this.name = "ForbiddenError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var ValidationError = class extends AppError {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(400, message);
|
|
25
|
+
this.name = "ValidationError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var ConflictError = class extends AppError {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(409, message);
|
|
31
|
+
this.name = "ConflictError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var LockedError = class extends AppError {
|
|
35
|
+
constructor(message = "Locked") {
|
|
36
|
+
super(423, message);
|
|
37
|
+
this.name = "LockedError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
AppError,
|
|
43
|
+
NotFoundError,
|
|
44
|
+
ForbiddenError,
|
|
45
|
+
ValidationError,
|
|
46
|
+
ConflictError,
|
|
47
|
+
LockedError
|
|
48
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ValidationError
|
|
3
|
+
} from "./chunk-3JTODC3Y.js";
|
|
4
|
+
import {
|
|
5
|
+
getDb
|
|
6
|
+
} from "./chunk-BC6KFUZH.js";
|
|
7
|
+
|
|
8
|
+
// src/governance/grants-service.ts
|
|
9
|
+
import { v4 as uuid } from "uuid";
|
|
10
|
+
var rank = (r) => r === "write" ? 2 : 1;
|
|
11
|
+
async function resolveNodeGrant(nestId, userId, nodeId) {
|
|
12
|
+
if (!userId || !nodeId) return null;
|
|
13
|
+
const rows = await getDb().all(
|
|
14
|
+
"SELECT target, role FROM grants WHERE nest_id = ? AND user_id = ?",
|
|
15
|
+
[nestId, userId]
|
|
16
|
+
);
|
|
17
|
+
let best = null;
|
|
18
|
+
for (const r of rows) {
|
|
19
|
+
if (nodeId === r.target || nodeId.startsWith(r.target + "/")) {
|
|
20
|
+
if (!best || rank(r.role) > rank(best)) best = r.role;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return best;
|
|
24
|
+
}
|
|
25
|
+
async function hasAnyGrant(nestId, userId) {
|
|
26
|
+
if (!userId) return false;
|
|
27
|
+
const row = await getDb().get(
|
|
28
|
+
"SELECT 1 AS x FROM grants WHERE nest_id = ? AND user_id = ? LIMIT 1",
|
|
29
|
+
[nestId, userId]
|
|
30
|
+
);
|
|
31
|
+
return !!row;
|
|
32
|
+
}
|
|
33
|
+
async function listUserGrants(nestId, userId) {
|
|
34
|
+
if (!userId) return [];
|
|
35
|
+
return await getDb().all(
|
|
36
|
+
"SELECT target, role FROM grants WHERE nest_id = ? AND user_id = ?",
|
|
37
|
+
[nestId, userId]
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function grantCoversNode(grants, nodeId) {
|
|
41
|
+
return grants.some(
|
|
42
|
+
(g) => nodeId === g.target || nodeId.startsWith(g.target + "/")
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
async function createGrant(params) {
|
|
46
|
+
const { nestId, targetType, target, userId, role, grantedBy } = params;
|
|
47
|
+
if (!["document", "folder"].includes(targetType)) {
|
|
48
|
+
throw new ValidationError("target_type must be document | folder");
|
|
49
|
+
}
|
|
50
|
+
if (!["read", "write"].includes(role)) {
|
|
51
|
+
throw new ValidationError("role must be read | write");
|
|
52
|
+
}
|
|
53
|
+
if (!target.trim()) throw new ValidationError("target is required");
|
|
54
|
+
if (targetType === "folder") {
|
|
55
|
+
const t = target.trim().replace(/\/+$/, "");
|
|
56
|
+
if (t === "nodes" || t === "") {
|
|
57
|
+
throw new ValidationError("folder target must name a subfolder, not the nest root");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const db = getDb();
|
|
61
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
62
|
+
const existing = await db.get(
|
|
63
|
+
"SELECT id FROM grants WHERE nest_id = ? AND target_type = ? AND target = ? AND user_id = ?",
|
|
64
|
+
[nestId, targetType, target, userId]
|
|
65
|
+
);
|
|
66
|
+
if (existing) {
|
|
67
|
+
await db.run(
|
|
68
|
+
"UPDATE grants SET role = ?, granted_by = ?, updated_at = ? WHERE id = ?",
|
|
69
|
+
[role, grantedBy, now, existing.id]
|
|
70
|
+
);
|
|
71
|
+
return await db.get("SELECT * FROM grants WHERE id = ?", [existing.id]);
|
|
72
|
+
}
|
|
73
|
+
const id = uuid();
|
|
74
|
+
await db.run(
|
|
75
|
+
`INSERT INTO grants (id, nest_id, target_type, target, user_id, role, granted_by, created_at, updated_at)
|
|
76
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
77
|
+
[id, nestId, targetType, target, userId, role, grantedBy, now, now]
|
|
78
|
+
);
|
|
79
|
+
return await db.get("SELECT * FROM grants WHERE id = ?", [id]);
|
|
80
|
+
}
|
|
81
|
+
async function listGrants(nestId) {
|
|
82
|
+
return await getDb().all(
|
|
83
|
+
"SELECT * FROM grants WHERE nest_id = ? ORDER BY target_type, target",
|
|
84
|
+
[nestId]
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
async function deleteGrant(nestId, id) {
|
|
88
|
+
const db = getDb();
|
|
89
|
+
const row = await db.get("SELECT id FROM grants WHERE id = ? AND nest_id = ?", [id, nestId]);
|
|
90
|
+
if (!row) return false;
|
|
91
|
+
await db.run("DELETE FROM grants WHERE id = ?", [id]);
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
resolveNodeGrant,
|
|
97
|
+
hasAnyGrant,
|
|
98
|
+
listUserGrants,
|
|
99
|
+
grantCoversNode,
|
|
100
|
+
createGrant,
|
|
101
|
+
listGrants,
|
|
102
|
+
deleteGrant
|
|
103
|
+
};
|