@promptowl/contextnest-community 1.8.0 → 1.9.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 +9 -0
- package/dist/chunk-3JTODC3Y.js +48 -0
- package/dist/{chunk-PJTAUVD7.js → chunk-BYS4HDME.js} +39 -53
- package/dist/{chunk-PLAHCDQG.js → chunk-DPHV6Q26.js} +253 -19
- package/dist/chunk-FRQJWGN3.js +8 -0
- package/dist/{chunk-XNGOO6RI.js → chunk-HRQWNRVI.js} +4 -2
- package/dist/chunk-TIO5XOFD.js +103 -0
- package/dist/chunk-VC3QNHHB.js +664 -0
- package/dist/chunk-XQ46F76G.js +16 -0
- package/dist/client-GI74NPIW.js +11 -0
- package/dist/email-R7DFS6E5.js +6 -0
- package/dist/grants-service-MDLRPMZR.js +21 -0
- package/dist/index.js +1862 -223
- package/dist/{migrations.postgres-VCG45LMK.js → migrations.postgres-ORSV7UFJ.js} +105 -1
- package/dist/{review-service-G5US3SMG.js → review-service-A3XFQTJH.js} +6 -4
- package/dist/{stewardship-service-ZXAMFTXO.js → stewardship-service-3KCVVXCW.js} +4 -2
- package/dist/{version-service-B3SDOJQE.js → version-service-6J4OWQL6.js} +3 -2
- package/dist/web3/assets/index-CHavHPIT.css +1 -0
- package/dist/web3/assets/index-DyvOBH92.js +934 -0
- package/dist/web3/index.html +2 -2
- package/package.json +14 -5
- 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/dist/index.js
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
generateApiKey,
|
|
4
|
-
getKeyPrefix,
|
|
5
|
-
hashApiKey,
|
|
6
|
-
hashPassword,
|
|
7
|
-
parseBearerToken,
|
|
8
|
-
verifyPassword
|
|
9
|
-
} from "./chunk-XRK6SQSC.js";
|
|
10
2
|
import {
|
|
11
3
|
approve,
|
|
12
4
|
cancelReview,
|
|
13
5
|
getPendingReview,
|
|
14
6
|
getReviewHistory,
|
|
15
7
|
getReviewQueue,
|
|
16
|
-
|
|
8
|
+
notifyNestEvent,
|
|
17
9
|
reject,
|
|
18
10
|
safePublishDocument,
|
|
19
11
|
submitForReview
|
|
20
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-VC3QNHHB.js";
|
|
13
|
+
import {
|
|
14
|
+
createGrant,
|
|
15
|
+
deleteGrant,
|
|
16
|
+
grantCoversNode,
|
|
17
|
+
hasAnyGrant,
|
|
18
|
+
listGrants,
|
|
19
|
+
listUserGrants,
|
|
20
|
+
resolveNodeGrant
|
|
21
|
+
} from "./chunk-TIO5XOFD.js";
|
|
22
|
+
import {
|
|
23
|
+
generateApiKey,
|
|
24
|
+
getKeyPrefix,
|
|
25
|
+
hashApiKey,
|
|
26
|
+
hashPassword,
|
|
27
|
+
parseBearerToken,
|
|
28
|
+
verifyPassword
|
|
29
|
+
} from "./chunk-XRK6SQSC.js";
|
|
30
|
+
import {
|
|
31
|
+
normalizeEmail
|
|
32
|
+
} from "./chunk-FRQJWGN3.js";
|
|
21
33
|
import {
|
|
22
34
|
checkConflict,
|
|
23
35
|
createVersion,
|
|
@@ -26,14 +38,8 @@ import {
|
|
|
26
38
|
getDisplayStatus,
|
|
27
39
|
getVersions,
|
|
28
40
|
setApprovedVersion
|
|
29
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-HRQWNRVI.js";
|
|
30
42
|
import {
|
|
31
|
-
AppError,
|
|
32
|
-
ConflictError,
|
|
33
|
-
ForbiddenError,
|
|
34
|
-
LockedError,
|
|
35
|
-
NotFoundError,
|
|
36
|
-
ValidationError,
|
|
37
43
|
canCreateInNest,
|
|
38
44
|
canManageStewards,
|
|
39
45
|
canUserAccess,
|
|
@@ -44,6 +50,7 @@ import {
|
|
|
44
50
|
createStewardRecord,
|
|
45
51
|
deleteNest,
|
|
46
52
|
disableStewardshipAndWipeGovernance,
|
|
53
|
+
docLink,
|
|
47
54
|
engineCache,
|
|
48
55
|
getCollaboratorRole,
|
|
49
56
|
getCurrentLicense,
|
|
@@ -66,6 +73,7 @@ import {
|
|
|
66
73
|
listStewards,
|
|
67
74
|
loadAccessConfig,
|
|
68
75
|
nestAllowsSelfApprove,
|
|
76
|
+
nestName,
|
|
69
77
|
nestStorageRoot,
|
|
70
78
|
permissionLevel,
|
|
71
79
|
removeSteward,
|
|
@@ -85,14 +93,26 @@ import {
|
|
|
85
93
|
updateSteward,
|
|
86
94
|
upsertEnvVar,
|
|
87
95
|
validateLicense
|
|
88
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-BYS4HDME.js";
|
|
97
|
+
import {
|
|
98
|
+
AppError,
|
|
99
|
+
ConflictError,
|
|
100
|
+
ForbiddenError,
|
|
101
|
+
LockedError,
|
|
102
|
+
NotFoundError,
|
|
103
|
+
ValidationError
|
|
104
|
+
} from "./chunk-3JTODC3Y.js";
|
|
105
|
+
import {
|
|
106
|
+
insertOrIgnore,
|
|
107
|
+
nowExpr
|
|
108
|
+
} from "./chunk-XQ46F76G.js";
|
|
89
109
|
import {
|
|
90
110
|
config,
|
|
91
111
|
getDb,
|
|
92
112
|
initDb,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
} from "./chunk-
|
|
113
|
+
isEmailListish,
|
|
114
|
+
isEmailish
|
|
115
|
+
} from "./chunk-DPHV6Q26.js";
|
|
96
116
|
import {
|
|
97
117
|
ANON_EMAIL,
|
|
98
118
|
ANON_USER_ID
|
|
@@ -102,7 +122,7 @@ import {
|
|
|
102
122
|
import { serve } from "@hono/node-server";
|
|
103
123
|
|
|
104
124
|
// src/app.ts
|
|
105
|
-
import { Hono as
|
|
125
|
+
import { Hono as Hono16 } from "hono";
|
|
106
126
|
import { LinearRouter } from "hono/router/linear-router";
|
|
107
127
|
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
108
128
|
import { cors } from "hono/cors";
|
|
@@ -229,11 +249,6 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
229
249
|
return c.json({ error: "Missing or invalid credentials" }, 401);
|
|
230
250
|
});
|
|
231
251
|
|
|
232
|
-
// src/shared/email.ts
|
|
233
|
-
function normalizeEmail(email) {
|
|
234
|
-
return email.trim().toLowerCase();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
252
|
// src/shared/rate-limit.ts
|
|
238
253
|
var buckets = /* @__PURE__ */ new Map();
|
|
239
254
|
function liveBucket(key, cutoff) {
|
|
@@ -1086,7 +1101,8 @@ async function canReadNode(nestId, nodeId, userId, userEmail) {
|
|
|
1086
1101
|
return await getApprovedVersion(nestId, nodeId) !== null;
|
|
1087
1102
|
}
|
|
1088
1103
|
if (!await isStewardshipEnabled(nestId)) return true;
|
|
1089
|
-
|
|
1104
|
+
if ((await canUserAccess(nestId, nodeId, userEmail)).allowed) return true;
|
|
1105
|
+
return await resolveNodeGrant(nestId, userId, nodeId) !== null;
|
|
1090
1106
|
}
|
|
1091
1107
|
async function filterAccessible(nestId, userId, userEmail, nodes) {
|
|
1092
1108
|
if (await isPublicReader(nestId, userId)) {
|
|
@@ -1099,9 +1115,10 @@ async function filterAccessible(nestId, userId, userEmail, nodes) {
|
|
|
1099
1115
|
return filtered;
|
|
1100
1116
|
}
|
|
1101
1117
|
if (!await isStewardshipEnabled(nestId)) return nodes;
|
|
1118
|
+
const grants = await listUserGrants(nestId, userId);
|
|
1102
1119
|
const accessible = [];
|
|
1103
1120
|
for (const n of nodes) {
|
|
1104
|
-
if ((await canUserAccess(nestId, n.id, userEmail)).allowed) {
|
|
1121
|
+
if ((await canUserAccess(nestId, n.id, userEmail)).allowed || grantCoversNode(grants, n.id)) {
|
|
1105
1122
|
accessible.push(n);
|
|
1106
1123
|
}
|
|
1107
1124
|
}
|
|
@@ -1291,7 +1308,7 @@ async function approveExternalEdit(input) {
|
|
|
1291
1308
|
const node = await storage.readDocument(input.documentId);
|
|
1292
1309
|
const versionNum = result.versionEntry.version;
|
|
1293
1310
|
const tags = node.frontmatter.tags || [];
|
|
1294
|
-
const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-
|
|
1311
|
+
const { createVersion: createVersion2, setApprovedVersion: setApprovedVersion2 } = await import("./version-service-6J4OWQL6.js");
|
|
1295
1312
|
await createVersion2({
|
|
1296
1313
|
nestId: input.nestId,
|
|
1297
1314
|
nodeId: input.documentId,
|
|
@@ -1419,7 +1436,11 @@ async function listNodesForCaller(nestId, userId, filters = {}) {
|
|
|
1419
1436
|
);
|
|
1420
1437
|
}
|
|
1421
1438
|
const userEmail = await resolveCallerEmail(userId);
|
|
1422
|
-
|
|
1439
|
+
let accessible = await filterAccessible(nestId, userId, userEmail, documents);
|
|
1440
|
+
if (await resolveNestPermission(nestId, userId) === "none" && !await isStewardshipEnabled(nestId)) {
|
|
1441
|
+
const grants = await listUserGrants(nestId, userId);
|
|
1442
|
+
accessible = grants.length ? documents.filter((d) => grantCoversNode(grants, d.id)) : accessible;
|
|
1443
|
+
}
|
|
1423
1444
|
const publicReader = await isPublicReader(nestId, userId);
|
|
1424
1445
|
const enriched = await Promise.all(
|
|
1425
1446
|
accessible.map(async (doc) => {
|
|
@@ -1566,9 +1587,18 @@ async function registerImportedDocuments(nestId, userEmail) {
|
|
|
1566
1587
|
return 0;
|
|
1567
1588
|
}
|
|
1568
1589
|
let registered = 0;
|
|
1569
|
-
for (
|
|
1590
|
+
for (let doc of docs) {
|
|
1570
1591
|
const nodeId = doc.id;
|
|
1571
1592
|
if (await getCurrentVersion(nestId, nodeId) > 0) continue;
|
|
1593
|
+
if (!doc.frontmatter?.title) {
|
|
1594
|
+
const title = nodeId.split("/").pop() || nodeId;
|
|
1595
|
+
doc = { ...doc, frontmatter: { ...doc.frontmatter, title } };
|
|
1596
|
+
try {
|
|
1597
|
+
await storage.writeDocument(nodeId, serializeDocument(doc));
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
console.error("import: failed to persist filename title", nodeId, err);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1572
1602
|
const rawTags = Array.isArray(doc.frontmatter?.tags) ? doc.frontmatter.tags : [];
|
|
1573
1603
|
const tags = rawTags.map((t) => normalizeTag2(String(t)));
|
|
1574
1604
|
const fmVersion = Number(doc.frontmatter?.version);
|
|
@@ -1881,13 +1911,13 @@ async function syncUnsyncedFolder(userId, folderName, callerEmail) {
|
|
|
1881
1911
|
}
|
|
1882
1912
|
const segments = folderName.split("/").filter(Boolean);
|
|
1883
1913
|
const baseName = segments[segments.length - 1] || folderName;
|
|
1884
|
-
const
|
|
1885
|
-
if (
|
|
1914
|
+
const nestName2 = await uniqueNestName(userId, baseName);
|
|
1915
|
+
if (nestName2 !== baseName) {
|
|
1886
1916
|
console.log(
|
|
1887
|
-
`[unsynced] name "${baseName}" already in use, using "${
|
|
1917
|
+
`[unsynced] name "${baseName}" already in use, using "${nestName2}" instead`
|
|
1888
1918
|
);
|
|
1889
1919
|
}
|
|
1890
|
-
const nest = await importNest(userId,
|
|
1920
|
+
const nest = await importNest(userId, nestName2, files);
|
|
1891
1921
|
console.log(
|
|
1892
1922
|
`[unsynced] nest created id=${nest.id} name="${nest.name}" from folder="${folderName}"`
|
|
1893
1923
|
);
|
|
@@ -1945,7 +1975,14 @@ nestRoutes.get("/", async (c) => {
|
|
|
1945
1975
|
const row = await db.get(ownerEmailSql, [n.user_id]);
|
|
1946
1976
|
owner_email = row?.email ?? null;
|
|
1947
1977
|
}
|
|
1948
|
-
return {
|
|
1978
|
+
return {
|
|
1979
|
+
...n,
|
|
1980
|
+
permission,
|
|
1981
|
+
is_owner,
|
|
1982
|
+
owner_email,
|
|
1983
|
+
roles,
|
|
1984
|
+
stewardship_enabled: await isStewardshipEnabled(n.id)
|
|
1985
|
+
};
|
|
1949
1986
|
};
|
|
1950
1987
|
const seen = /* @__PURE__ */ new Set();
|
|
1951
1988
|
const out = [];
|
|
@@ -2197,9 +2234,19 @@ async function addCollaborator(params) {
|
|
|
2197
2234
|
"INSERT INTO nest_collaborators (id, nest_id, user_id, permission, granted_by) VALUES (?, ?, ?, ?, ?)",
|
|
2198
2235
|
[collabId, nestId, userId, params.permission, granterId]
|
|
2199
2236
|
);
|
|
2200
|
-
|
|
2237
|
+
const shareNestName = await nestName(nestId);
|
|
2238
|
+
void notifyNestEvent(
|
|
2201
2239
|
nestId,
|
|
2202
|
-
`:key: ${params.email || userId} added as *${params.permission}*${params.grantedByEmail ? ` by ${params.grantedByEmail}` : ""}
|
|
2240
|
+
`:key: ${params.email || userId} added as *${params.permission}*${params.grantedByEmail ? ` by ${params.grantedByEmail}` : ""}`,
|
|
2241
|
+
{
|
|
2242
|
+
status: "shared",
|
|
2243
|
+
docTitle: shareNestName,
|
|
2244
|
+
path: shareNestName,
|
|
2245
|
+
link: docLink(nestId, void 0, params.baseUrl),
|
|
2246
|
+
actor: params.email || userId,
|
|
2247
|
+
permission: params.permission,
|
|
2248
|
+
by: params.grantedByEmail || void 0
|
|
2249
|
+
}
|
|
2203
2250
|
);
|
|
2204
2251
|
return await db.get(
|
|
2205
2252
|
"SELECT * FROM nest_collaborators WHERE id = ?",
|
|
@@ -2207,6 +2254,16 @@ async function addCollaborator(params) {
|
|
|
2207
2254
|
);
|
|
2208
2255
|
}
|
|
2209
2256
|
|
|
2257
|
+
// src/shared/base-url.ts
|
|
2258
|
+
function requestBaseUrl(reqUrl) {
|
|
2259
|
+
if (config.PUBLIC_BASE_URL) return config.PUBLIC_BASE_URL;
|
|
2260
|
+
try {
|
|
2261
|
+
return new URL(reqUrl).origin;
|
|
2262
|
+
} catch {
|
|
2263
|
+
return "";
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2210
2267
|
// src/nests/sharing-routes.ts
|
|
2211
2268
|
var sharingRoutes = new Hono3();
|
|
2212
2269
|
sharingRoutes.get("/collaborators", async (c) => {
|
|
@@ -2243,7 +2300,8 @@ sharingRoutes.post("/collaborators", async (c) => {
|
|
|
2243
2300
|
grantedByUserId: c.get("userId"),
|
|
2244
2301
|
// Enforce the escalation cap against the caller's own nest permission
|
|
2245
2302
|
// (set by the access guard) — a write collaborator can't grant admin.
|
|
2246
|
-
callerPermission: c.get("nestPermission")
|
|
2303
|
+
callerPermission: c.get("nestPermission"),
|
|
2304
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
2247
2305
|
});
|
|
2248
2306
|
return c.json({ collaborator: collab }, 201);
|
|
2249
2307
|
});
|
|
@@ -2279,15 +2337,93 @@ sharingRoutes.patch("/visibility", async (c) => {
|
|
|
2279
2337
|
throw new ValidationError("visibility must be private or public");
|
|
2280
2338
|
}
|
|
2281
2339
|
const db = getDb();
|
|
2282
|
-
|
|
2340
|
+
const nestId = c.req.param("nestId");
|
|
2341
|
+
const result = await db.run("UPDATE nests SET visibility = ? WHERE id = ?", [
|
|
2283
2342
|
body.visibility,
|
|
2284
|
-
|
|
2343
|
+
nestId
|
|
2344
|
+
]);
|
|
2345
|
+
if (result.changes === 0) {
|
|
2346
|
+
throw new NotFoundError("Nest not found");
|
|
2347
|
+
}
|
|
2348
|
+
const row = await db.get("SELECT visibility FROM nests WHERE id = ?", [
|
|
2349
|
+
nestId
|
|
2285
2350
|
]);
|
|
2286
|
-
return c.json({ visibility:
|
|
2351
|
+
return c.json({ visibility: row.visibility });
|
|
2287
2352
|
});
|
|
2288
2353
|
|
|
2289
|
-
// src/
|
|
2354
|
+
// src/nests/grant-routes.ts
|
|
2290
2355
|
import { Hono as Hono4 } from "hono";
|
|
2356
|
+
import { v4 as uuid3 } from "uuid";
|
|
2357
|
+
var grantRoutes = new Hono4();
|
|
2358
|
+
async function requireManage(c, nestId) {
|
|
2359
|
+
if (!await canManageStewards(nestId, c.get("userId"))) {
|
|
2360
|
+
throw new ForbiddenError(
|
|
2361
|
+
"Only a nest admin, the nest owner, or the server admin can manage sharing."
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
async function resolveGrantee(email) {
|
|
2366
|
+
const db = getDb();
|
|
2367
|
+
const e = normalizeEmail(email);
|
|
2368
|
+
const existing = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
2369
|
+
if (existing) return existing.id;
|
|
2370
|
+
const { hashPassword: hashPassword2 } = await import("./keys-73STFJJB.js");
|
|
2371
|
+
const id = uuid3();
|
|
2372
|
+
await db.run(
|
|
2373
|
+
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
2374
|
+
[id, e, null, await hashPassword2(uuid3())]
|
|
2375
|
+
);
|
|
2376
|
+
return id;
|
|
2377
|
+
}
|
|
2378
|
+
grantRoutes.post("/", async (c) => {
|
|
2379
|
+
const nestId = c.req.param("nestId");
|
|
2380
|
+
await requireManage(c, nestId);
|
|
2381
|
+
const body = await c.req.json();
|
|
2382
|
+
const targetType = body.target_type;
|
|
2383
|
+
const target = (body.target || "").trim();
|
|
2384
|
+
const role = body.role || "read";
|
|
2385
|
+
if (!["document", "folder"].includes(targetType)) {
|
|
2386
|
+
throw new ValidationError("target_type must be document | folder");
|
|
2387
|
+
}
|
|
2388
|
+
if (!target) throw new ValidationError("target is required");
|
|
2389
|
+
if (!body.email?.trim()) throw new ValidationError("email is required");
|
|
2390
|
+
if (!["read", "write"].includes(role)) {
|
|
2391
|
+
throw new ValidationError("role must be read | write");
|
|
2392
|
+
}
|
|
2393
|
+
const userId = await resolveGrantee(body.email);
|
|
2394
|
+
const grant = await createGrant({
|
|
2395
|
+
nestId,
|
|
2396
|
+
targetType,
|
|
2397
|
+
target,
|
|
2398
|
+
userId,
|
|
2399
|
+
role,
|
|
2400
|
+
grantedBy: await resolveCallerEmail(c.get("userId"))
|
|
2401
|
+
});
|
|
2402
|
+
return c.json({ grant }, 201);
|
|
2403
|
+
});
|
|
2404
|
+
grantRoutes.get("/", async (c) => {
|
|
2405
|
+
const nestId = c.req.param("nestId");
|
|
2406
|
+
await requireManage(c, nestId);
|
|
2407
|
+
const grants = await listGrants(nestId);
|
|
2408
|
+
const db = getDb();
|
|
2409
|
+
const withEmail = await Promise.all(
|
|
2410
|
+
grants.map(async (g) => {
|
|
2411
|
+
const u = await db.get("SELECT email FROM users WHERE id = ?", [g.user_id]);
|
|
2412
|
+
return { ...g, email: u?.email ?? null };
|
|
2413
|
+
})
|
|
2414
|
+
);
|
|
2415
|
+
return c.json({ count: withEmail.length, grants: withEmail });
|
|
2416
|
+
});
|
|
2417
|
+
grantRoutes.delete("/:id", async (c) => {
|
|
2418
|
+
const nestId = c.req.param("nestId");
|
|
2419
|
+
await requireManage(c, nestId);
|
|
2420
|
+
const ok = await deleteGrant(nestId, c.req.param("id"));
|
|
2421
|
+
if (!ok) throw new NotFoundError("Grant not found");
|
|
2422
|
+
return c.json({ deleted: true });
|
|
2423
|
+
});
|
|
2424
|
+
|
|
2425
|
+
// src/nodes/routes.ts
|
|
2426
|
+
import { Hono as Hono5 } from "hono";
|
|
2291
2427
|
|
|
2292
2428
|
// src/nodes/markdown-export.ts
|
|
2293
2429
|
function nodeToMarkdown(node) {
|
|
@@ -2310,7 +2446,7 @@ function isMarkdownFormat(c) {
|
|
|
2310
2446
|
}
|
|
2311
2447
|
|
|
2312
2448
|
// src/annotations/service.ts
|
|
2313
|
-
import { v4 as
|
|
2449
|
+
import { v4 as uuid4 } from "uuid";
|
|
2314
2450
|
|
|
2315
2451
|
// src/annotations/projection.ts
|
|
2316
2452
|
var MAX_CONTEXT_CHARS = 250;
|
|
@@ -2460,7 +2596,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
2460
2596
|
throw new Error("comment body is required");
|
|
2461
2597
|
}
|
|
2462
2598
|
const db = getDb();
|
|
2463
|
-
const id =
|
|
2599
|
+
const id = uuid4();
|
|
2464
2600
|
const anchor = clampAnchor(input.anchor);
|
|
2465
2601
|
const snapshot = input.snapshotVersion ?? await getApprovedVersion(nestId, nodeId) ?? null;
|
|
2466
2602
|
await db.transaction(async (tx) => {
|
|
@@ -2479,7 +2615,7 @@ async function createThread(nestId, nodeId, input, authorEmail) {
|
|
|
2479
2615
|
);
|
|
2480
2616
|
await tx.run(
|
|
2481
2617
|
"INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
|
|
2482
|
-
[
|
|
2618
|
+
[uuid4(), id, authorEmail, body]
|
|
2483
2619
|
);
|
|
2484
2620
|
});
|
|
2485
2621
|
return rowToThread(await getThreadRow(id));
|
|
@@ -2499,7 +2635,7 @@ async function addComment(nestId, nodeId, threadId, authorEmail, body) {
|
|
|
2499
2635
|
await getScopedThreadRow(threadId, nestId, nodeId);
|
|
2500
2636
|
await getDb().run(
|
|
2501
2637
|
"INSERT INTO annotation_comments (id, thread_id, author, body) VALUES (?, ?, ?, ?)",
|
|
2502
|
-
[
|
|
2638
|
+
[uuid4(), threadId, authorEmail, trimmed]
|
|
2503
2639
|
);
|
|
2504
2640
|
return rowToThread(await getThreadRow(threadId));
|
|
2505
2641
|
}
|
|
@@ -2598,7 +2734,7 @@ async function syncAnnotationsNode(nestId, nodeId, userEmail) {
|
|
|
2598
2734
|
}
|
|
2599
2735
|
|
|
2600
2736
|
// src/nodes/routes.ts
|
|
2601
|
-
var nodeRoutes = new
|
|
2737
|
+
var nodeRoutes = new Hono5();
|
|
2602
2738
|
function nodeAsMarkdown(response, nodeId) {
|
|
2603
2739
|
return nodeToMarkdown({
|
|
2604
2740
|
id: nodeId,
|
|
@@ -2651,7 +2787,7 @@ nodeRoutes.post("/", async (c) => {
|
|
|
2651
2787
|
nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
|
|
2652
2788
|
const nestId = c.req.param("nestId");
|
|
2653
2789
|
const nodeId = c.req.param("nodeId");
|
|
2654
|
-
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-
|
|
2790
|
+
const { resolveStewardsWithFallback: resolveStewardsWithFallback2 } = await import("./stewardship-service-3KCVVXCW.js");
|
|
2655
2791
|
const { stewards, fallbackToOwner, ownerEmail } = await resolveStewardsWithFallback2(
|
|
2656
2792
|
nestId,
|
|
2657
2793
|
nodeId
|
|
@@ -2672,7 +2808,7 @@ nodeRoutes.get("/:nodeId{.+?}/stewards", async (c) => {
|
|
|
2672
2808
|
nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
|
|
2673
2809
|
const nestId = c.req.param("nestId");
|
|
2674
2810
|
const nodeId = c.req.param("nodeId");
|
|
2675
|
-
const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-
|
|
2811
|
+
const { getVersions: getVersions2, getApprovedVersion: getApprovedVersion2 } = await import("./version-service-6J4OWQL6.js");
|
|
2676
2812
|
const allVersions = await getVersions2(nestId, nodeId);
|
|
2677
2813
|
const approved = await getApprovedVersion2(nestId, nodeId);
|
|
2678
2814
|
const db = getDb();
|
|
@@ -2704,7 +2840,7 @@ nodeRoutes.get("/:nodeId{.+?}/versions", async (c) => {
|
|
|
2704
2840
|
nodeRoutes.get("/:nodeId{.+?}/reviews", async (c) => {
|
|
2705
2841
|
const nestId = c.req.param("nestId");
|
|
2706
2842
|
const nodeId = c.req.param("nodeId");
|
|
2707
|
-
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-
|
|
2843
|
+
const { getReviewHistory: getReviewHistory2 } = await import("./review-service-A3XFQTJH.js");
|
|
2708
2844
|
const history = await getReviewHistory2(nestId, nodeId);
|
|
2709
2845
|
return c.json({ reviews: history });
|
|
2710
2846
|
});
|
|
@@ -2937,13 +3073,13 @@ async function getUserEmail(c) {
|
|
|
2937
3073
|
}
|
|
2938
3074
|
|
|
2939
3075
|
// src/annotations/routes.ts
|
|
2940
|
-
import { Hono as
|
|
3076
|
+
import { Hono as Hono6 } from "hono";
|
|
2941
3077
|
|
|
2942
3078
|
// src/annotations/types.ts
|
|
2943
3079
|
var ARTIFACT_NODE_TYPE = "artifact";
|
|
2944
3080
|
|
|
2945
3081
|
// src/annotations/routes.ts
|
|
2946
|
-
var annotationRoutes = new
|
|
3082
|
+
var annotationRoutes = new Hono6();
|
|
2947
3083
|
function getNodeId(c) {
|
|
2948
3084
|
const raw = c.req.param("nodeId");
|
|
2949
3085
|
try {
|
|
@@ -3070,7 +3206,7 @@ annotationRoutes.get("/:nodeId{.+}/hosted", async (c) => {
|
|
|
3070
3206
|
});
|
|
3071
3207
|
|
|
3072
3208
|
// src/nodes/query-routes.ts
|
|
3073
|
-
import { Hono as
|
|
3209
|
+
import { Hono as Hono7 } from "hono";
|
|
3074
3210
|
import { serializeDocument as serializeDocument2 } from "@promptowl/contextnest-engine";
|
|
3075
3211
|
|
|
3076
3212
|
// src/nodes/prompt-compiler.ts
|
|
@@ -3292,6 +3428,10 @@ async function resolveExportBody(nestId, nodeId, workingBody) {
|
|
|
3292
3428
|
// src/nodes/graph-service.ts
|
|
3293
3429
|
var MAX_GRAPH_NODES = 150;
|
|
3294
3430
|
var WIKILINK_RE = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
3431
|
+
function bareSlug(idOrTarget) {
|
|
3432
|
+
const idx = idOrTarget.lastIndexOf("/");
|
|
3433
|
+
return idx >= 0 ? idOrTarget.slice(idx + 1) : idOrTarget;
|
|
3434
|
+
}
|
|
3295
3435
|
function extractWikiTargets(body) {
|
|
3296
3436
|
const out = [];
|
|
3297
3437
|
if (!body) return out;
|
|
@@ -3309,9 +3449,12 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
3309
3449
|
const accessible = await filterAccessible(nestId, userId, userEmail, docs);
|
|
3310
3450
|
const idSet = new Set(accessible.map((d) => d.id));
|
|
3311
3451
|
const titleToId = /* @__PURE__ */ new Map();
|
|
3452
|
+
const slugToId = /* @__PURE__ */ new Map();
|
|
3312
3453
|
for (const d of accessible) {
|
|
3313
3454
|
const t = (d.frontmatter?.title || "").toLowerCase().trim();
|
|
3314
3455
|
if (t && !titleToId.has(t)) titleToId.set(t, d.id);
|
|
3456
|
+
const slug = bareSlug(d.id).toLowerCase();
|
|
3457
|
+
if (slug && !slugToId.has(slug)) slugToId.set(slug, d.id);
|
|
3315
3458
|
}
|
|
3316
3459
|
const allNodes = accessible.map((d) => ({
|
|
3317
3460
|
id: d.id,
|
|
@@ -3323,7 +3466,8 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
3323
3466
|
const seen = /* @__PURE__ */ new Set();
|
|
3324
3467
|
for (const d of accessible) {
|
|
3325
3468
|
for (const raw of extractWikiTargets(d.body || "")) {
|
|
3326
|
-
const
|
|
3469
|
+
const norm = raw.toLowerCase().trim();
|
|
3470
|
+
const targetId = idSet.has(raw) ? raw : titleToId.get(norm) ?? slugToId.get(bareSlug(raw).toLowerCase());
|
|
3327
3471
|
if (!targetId || targetId === d.id) continue;
|
|
3328
3472
|
const key = `${d.id}->${targetId}`;
|
|
3329
3473
|
if (seen.has(key)) continue;
|
|
@@ -3432,7 +3576,7 @@ async function buildNestGraph(nestId, userId, userEmail, canSeeIdentities) {
|
|
|
3432
3576
|
}
|
|
3433
3577
|
|
|
3434
3578
|
// src/nodes/query-routes.ts
|
|
3435
|
-
var queryRoutes = new
|
|
3579
|
+
var queryRoutes = new Hono7();
|
|
3436
3580
|
queryRoutes.get("/changes", async (c) => {
|
|
3437
3581
|
const nestId = c.req.param("nestId");
|
|
3438
3582
|
const userId = c.get("userId");
|
|
@@ -3533,6 +3677,23 @@ queryRoutes.get("/graph", async (c) => {
|
|
|
3533
3677
|
const userEmail = await resolveCallerEmail(userId);
|
|
3534
3678
|
const canSeeIdentities = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
|
|
3535
3679
|
const graph = await buildNestGraph(nestId, userId, userEmail, canSeeIdentities);
|
|
3680
|
+
if (config.FEATURE_WORKFLOW_PLANE) {
|
|
3681
|
+
const rows = await getDb().all(
|
|
3682
|
+
`SELECT e.from_node, e.to_node, e.condition_mode, t.name as type, t.color, t.is_flow
|
|
3683
|
+
FROM edges e JOIN edge_types t ON t.id = e.type_id
|
|
3684
|
+
WHERE e.nest_id = ?`,
|
|
3685
|
+
[nestId]
|
|
3686
|
+
);
|
|
3687
|
+
const visible = new Set(graph.nodes.map((n) => n.id));
|
|
3688
|
+
graph.typedEdges = rows.filter((r) => visible.has(r.from_node) && visible.has(r.to_node)).map((r) => ({
|
|
3689
|
+
source: r.from_node,
|
|
3690
|
+
target: r.to_node,
|
|
3691
|
+
type: r.type,
|
|
3692
|
+
color: r.color,
|
|
3693
|
+
is_flow: !!r.is_flow,
|
|
3694
|
+
condition_mode: r.condition_mode
|
|
3695
|
+
}));
|
|
3696
|
+
}
|
|
3536
3697
|
return c.json(graph);
|
|
3537
3698
|
});
|
|
3538
3699
|
function approxTokens(text) {
|
|
@@ -3914,7 +4075,7 @@ queryRoutes.post("/publish", async (c) => {
|
|
|
3914
4075
|
});
|
|
3915
4076
|
|
|
3916
4077
|
// src/mcp/routes.ts
|
|
3917
|
-
import { Hono as
|
|
4078
|
+
import { Hono as Hono10 } from "hono";
|
|
3918
4079
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3919
4080
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3920
4081
|
|
|
@@ -3991,110 +4152,1210 @@ async function listTraceEvents(filters = {}) {
|
|
|
3991
4152
|
}
|
|
3992
4153
|
}
|
|
3993
4154
|
|
|
3994
|
-
// src/mcp/tools.ts
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
}
|
|
4013
|
-
{
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
inputSchema: {
|
|
4017
|
-
type: "object",
|
|
4018
|
-
properties: {
|
|
4019
|
-
query: { type: "string", description: "Search terms" }
|
|
4020
|
-
},
|
|
4021
|
-
required: ["query"]
|
|
4022
|
-
}
|
|
4023
|
-
},
|
|
4024
|
-
{
|
|
4025
|
-
name: "context_query",
|
|
4026
|
-
description: "Run a structured selector query. Supports: #tag, type:X, [[Title]], scope:X. Combine with +AND, |OR, -NOT.",
|
|
4027
|
-
inputSchema: {
|
|
4028
|
-
type: "object",
|
|
4029
|
-
properties: {
|
|
4030
|
-
query: { type: "string", description: "Selector query" },
|
|
4031
|
-
hops: {
|
|
4032
|
-
type: "number",
|
|
4033
|
-
description: "Graph traversal depth from the matched nodes (default: 2). Use 1 for just the matches + direct links, higher to pull in more of the neighborhood."
|
|
4034
|
-
}
|
|
4035
|
-
},
|
|
4036
|
-
required: ["query"]
|
|
4155
|
+
// src/mcp/workflow-tools.ts
|
|
4156
|
+
import { v4 as uuid8 } from "uuid";
|
|
4157
|
+
|
|
4158
|
+
// src/shared/node-id.ts
|
|
4159
|
+
function assertSafeNodeId(rawId) {
|
|
4160
|
+
let id = rawId;
|
|
4161
|
+
try {
|
|
4162
|
+
id = decodeURIComponent(rawId);
|
|
4163
|
+
} catch {
|
|
4164
|
+
}
|
|
4165
|
+
if (!id || id.length > 512) {
|
|
4166
|
+
throw new ValidationError("invalid node id");
|
|
4167
|
+
}
|
|
4168
|
+
if (id.includes("\0") || id.includes("\\")) {
|
|
4169
|
+
throw new ValidationError("invalid node id");
|
|
4170
|
+
}
|
|
4171
|
+
if (id.startsWith("/")) {
|
|
4172
|
+
throw new ValidationError("invalid node id (absolute path)");
|
|
4173
|
+
}
|
|
4174
|
+
for (const seg of id.split("/")) {
|
|
4175
|
+
if (seg === "" || seg === "." || seg === "..") {
|
|
4176
|
+
throw new ValidationError("invalid node id (path traversal)");
|
|
4037
4177
|
}
|
|
4038
|
-
}
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4178
|
+
}
|
|
4179
|
+
return id;
|
|
4180
|
+
}
|
|
4181
|
+
|
|
4182
|
+
// src/workflow/run-service.ts
|
|
4183
|
+
import { v4 as uuid5 } from "uuid";
|
|
4184
|
+
|
|
4185
|
+
// src/shared/json.ts
|
|
4186
|
+
var safeJson = (s) => {
|
|
4187
|
+
if (!s) return null;
|
|
4188
|
+
try {
|
|
4189
|
+
return JSON.parse(s);
|
|
4190
|
+
} catch {
|
|
4191
|
+
return null;
|
|
4192
|
+
}
|
|
4193
|
+
};
|
|
4194
|
+
|
|
4195
|
+
// src/workflow/run-service.ts
|
|
4196
|
+
var RUNNABLE_TYPES = ["agent", "skill"];
|
|
4197
|
+
async function flowSubgraph(nestId, agentNode) {
|
|
4198
|
+
const db = getDb();
|
|
4199
|
+
const rows = await db.all(
|
|
4200
|
+
`SELECT e.id, e.from_node, e.to_node, e.condition_mode, e.condition, e.metadata,
|
|
4201
|
+
t.name as type_name, t.color as type_color
|
|
4202
|
+
FROM edges e JOIN edge_types t ON t.id = e.type_id
|
|
4203
|
+
WHERE e.nest_id = ? AND t.is_flow = 1`,
|
|
4204
|
+
[nestId]
|
|
4205
|
+
);
|
|
4206
|
+
const byFrom = /* @__PURE__ */ new Map();
|
|
4207
|
+
for (const r of rows) {
|
|
4208
|
+
if (!byFrom.has(r.from_node)) byFrom.set(r.from_node, []);
|
|
4209
|
+
byFrom.get(r.from_node).push(r);
|
|
4210
|
+
}
|
|
4211
|
+
const seen = /* @__PURE__ */ new Set([agentNode]);
|
|
4212
|
+
const queue = [agentNode];
|
|
4213
|
+
const edges = [];
|
|
4214
|
+
while (queue.length) {
|
|
4215
|
+
const cur = queue.shift();
|
|
4216
|
+
for (const e of byFrom.get(cur) ?? []) {
|
|
4217
|
+
edges.push(e);
|
|
4218
|
+
if (!seen.has(e.to_node)) {
|
|
4219
|
+
seen.add(e.to_node);
|
|
4220
|
+
queue.push(e.to_node);
|
|
4047
4221
|
}
|
|
4048
4222
|
}
|
|
4049
|
-
}
|
|
4050
|
-
{
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4223
|
+
}
|
|
4224
|
+
return edges.map((e) => ({
|
|
4225
|
+
id: e.id,
|
|
4226
|
+
from_node: e.from_node,
|
|
4227
|
+
to_node: e.to_node,
|
|
4228
|
+
type: { name: e.type_name, color: e.type_color },
|
|
4229
|
+
condition_mode: e.condition_mode,
|
|
4230
|
+
condition: e.condition_mode === "structured" ? safeJson(e.condition) : e.condition,
|
|
4231
|
+
metadata: safeJson(e.metadata)
|
|
4232
|
+
}));
|
|
4233
|
+
}
|
|
4234
|
+
async function referencedDefinitions(nestId, edges) {
|
|
4235
|
+
const terms = /* @__PURE__ */ new Set();
|
|
4236
|
+
for (const e of edges) {
|
|
4237
|
+
if (e.condition_mode === "structured" && e.condition && typeof e.condition === "object") {
|
|
4238
|
+
const t = e.condition.term;
|
|
4239
|
+
if (typeof t === "string" && t.trim()) terms.add(t.trim());
|
|
4060
4240
|
}
|
|
4061
|
-
}
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4241
|
+
}
|
|
4242
|
+
if (!terms.size) return [];
|
|
4243
|
+
try {
|
|
4244
|
+
const db = getDb();
|
|
4245
|
+
const list = [...terms];
|
|
4246
|
+
const placeholders = list.map(() => "LOWER(?)").join(", ");
|
|
4247
|
+
const rows = await db.all(
|
|
4248
|
+
`SELECT term, definition, linked_tag FROM definitions
|
|
4249
|
+
WHERE nest_id = ? AND LOWER(term) IN (${placeholders})`,
|
|
4250
|
+
[nestId, ...list]
|
|
4251
|
+
);
|
|
4252
|
+
return rows;
|
|
4253
|
+
} catch {
|
|
4254
|
+
return [];
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
async function resolveParentRun(nestId, parentRunId, childAgent) {
|
|
4258
|
+
if (!config.FEATURE_SUBAGENT_RUNS) {
|
|
4259
|
+
throw new ValidationError(
|
|
4260
|
+
"Sub-agent runs are not enabled on this server (FEATURE_SUBAGENT_RUNS)."
|
|
4261
|
+
);
|
|
4262
|
+
}
|
|
4263
|
+
const db = getDb();
|
|
4264
|
+
let cursor = parentRunId;
|
|
4265
|
+
let parentDepth = -1;
|
|
4266
|
+
for (let i = 0; i <= config.SUBAGENT_MAX_DEPTH + 1; i++) {
|
|
4267
|
+
if (!cursor) break;
|
|
4268
|
+
const row = await db.get(
|
|
4269
|
+
"SELECT id, nest_id, agent_node, status, depth, parent_run_id FROM runs WHERE id = ?",
|
|
4270
|
+
[cursor]
|
|
4271
|
+
);
|
|
4272
|
+
if (!row || row.nest_id !== nestId) {
|
|
4273
|
+
throw new NotFoundError(`Parent run not found: ${cursor}`);
|
|
4082
4274
|
}
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
inputSchema: {
|
|
4088
|
-
type: "object",
|
|
4089
|
-
properties: {
|
|
4090
|
-
max_tokens: {
|
|
4091
|
-
type: "number",
|
|
4092
|
-
description: "Approximate token budget; documents are included until it's reached (omit for no cap)."
|
|
4093
|
-
}
|
|
4094
|
-
}
|
|
4275
|
+
if (cursor === parentRunId && row.status !== "running") {
|
|
4276
|
+
throw new ConflictError(
|
|
4277
|
+
`Parent run is ${row.status} \u2014 cannot spawn a sub-agent under a closed run`
|
|
4278
|
+
);
|
|
4095
4279
|
}
|
|
4096
|
-
|
|
4097
|
-
|
|
4280
|
+
if (row.agent_node === childAgent) {
|
|
4281
|
+
throw new ConflictError(
|
|
4282
|
+
`re-entrancy: "${childAgent}" is already running in this call chain \u2014 sub-agent recursion must be acyclic`
|
|
4283
|
+
);
|
|
4284
|
+
}
|
|
4285
|
+
if (cursor === parentRunId) parentDepth = row.depth;
|
|
4286
|
+
cursor = row.parent_run_id;
|
|
4287
|
+
}
|
|
4288
|
+
const depth = parentDepth + 1;
|
|
4289
|
+
if (depth > config.SUBAGENT_MAX_DEPTH) {
|
|
4290
|
+
throw new ForbiddenError(
|
|
4291
|
+
`sub-agent depth ${depth} exceeds the limit of ${config.SUBAGENT_MAX_DEPTH}`
|
|
4292
|
+
);
|
|
4293
|
+
}
|
|
4294
|
+
return { depth };
|
|
4295
|
+
}
|
|
4296
|
+
async function loadRun(runId) {
|
|
4297
|
+
return await getDb().get("SELECT * FROM runs WHERE id = ?", [runId]);
|
|
4298
|
+
}
|
|
4299
|
+
async function triggerRun(opts) {
|
|
4300
|
+
const { nestId, inputs, triggeredBy, storage } = opts;
|
|
4301
|
+
const agentNode = assertSafeNodeId(opts.agentNode);
|
|
4302
|
+
let doc;
|
|
4303
|
+
try {
|
|
4304
|
+
doc = await storage.readDocument(agentNode);
|
|
4305
|
+
} catch {
|
|
4306
|
+
throw new NotFoundError(`Agent node not found: ${agentNode}`);
|
|
4307
|
+
}
|
|
4308
|
+
const nodeType = doc.frontmatter.type || "document";
|
|
4309
|
+
if (!RUNNABLE_TYPES.includes(nodeType)) {
|
|
4310
|
+
throw new ValidationError(
|
|
4311
|
+
`"${agentNode}" is type "${nodeType}" \u2014 only agent/skill nodes are runnable`
|
|
4312
|
+
);
|
|
4313
|
+
}
|
|
4314
|
+
let parentRunId = null;
|
|
4315
|
+
let depth = 0;
|
|
4316
|
+
if (opts.parentRunId !== void 0 && opts.parentRunId !== null) {
|
|
4317
|
+
if (typeof opts.parentRunId !== "string") {
|
|
4318
|
+
throw new ValidationError("parent_run_id must be a string");
|
|
4319
|
+
}
|
|
4320
|
+
({ depth } = await resolveParentRun(nestId, opts.parentRunId, agentNode));
|
|
4321
|
+
parentRunId = opts.parentRunId;
|
|
4322
|
+
}
|
|
4323
|
+
const edges = await flowSubgraph(nestId, agentNode);
|
|
4324
|
+
const definitions = await referencedDefinitions(nestId, edges);
|
|
4325
|
+
const id = `run_${uuid5()}`;
|
|
4326
|
+
const db = getDb();
|
|
4327
|
+
await db.transaction(async (tx) => {
|
|
4328
|
+
if (parentRunId) {
|
|
4329
|
+
if (db.dialect === "postgres") {
|
|
4330
|
+
await tx.get("SELECT id FROM runs WHERE id = ? FOR UPDATE", [parentRunId]);
|
|
4331
|
+
}
|
|
4332
|
+
const kids = await tx.get(
|
|
4333
|
+
"SELECT COUNT(*) as c FROM runs WHERE parent_run_id = ?",
|
|
4334
|
+
[parentRunId]
|
|
4335
|
+
);
|
|
4336
|
+
if (Number(kids.c) >= config.SUBAGENT_MAX_CHILDREN) {
|
|
4337
|
+
throw new ForbiddenError(
|
|
4338
|
+
`parent run already has ${kids.c} children (limit ${config.SUBAGENT_MAX_CHILDREN})`
|
|
4339
|
+
);
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
await tx.run(
|
|
4343
|
+
`INSERT INTO runs (id, nest_id, agent_node, triggered_by, status, inputs, started_at, parent_run_id, depth)
|
|
4344
|
+
VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)`,
|
|
4345
|
+
[
|
|
4346
|
+
id,
|
|
4347
|
+
nestId,
|
|
4348
|
+
agentNode,
|
|
4349
|
+
triggeredBy,
|
|
4350
|
+
inputs === void 0 ? null : JSON.stringify(inputs),
|
|
4351
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
4352
|
+
parentRunId,
|
|
4353
|
+
depth
|
|
4354
|
+
]
|
|
4355
|
+
);
|
|
4356
|
+
});
|
|
4357
|
+
return {
|
|
4358
|
+
run_id: id,
|
|
4359
|
+
parent_run_id: parentRunId,
|
|
4360
|
+
depth,
|
|
4361
|
+
agent: {
|
|
4362
|
+
id: agentNode,
|
|
4363
|
+
title: String(doc.frontmatter.title ?? agentNode),
|
|
4364
|
+
type: nodeType,
|
|
4365
|
+
schedule: doc.frontmatter.metadata?.schedule ?? null,
|
|
4366
|
+
content: doc.body || ""
|
|
4367
|
+
},
|
|
4368
|
+
edges,
|
|
4369
|
+
definitions,
|
|
4370
|
+
trace_hint: {
|
|
4371
|
+
retrieval: "POST /context is deterministic \u2014 record selector+hops per read so the run replays to the identical read-set",
|
|
4372
|
+
steps: "POST /runs/{run_id}/steps per action; PATCH /runs/{run_id} to close"
|
|
4373
|
+
}
|
|
4374
|
+
};
|
|
4375
|
+
}
|
|
4376
|
+
var STEP_ACTIONS = ["read", "write", "branch", "notify", "external"];
|
|
4377
|
+
async function appendRunStep(runId, input) {
|
|
4378
|
+
if (typeof input.action !== "string" || !input.action.trim()) {
|
|
4379
|
+
throw new ValidationError("action is required (read|write|branch|notify|external)");
|
|
4380
|
+
}
|
|
4381
|
+
const action = input.action.trim();
|
|
4382
|
+
if (!STEP_ACTIONS.includes(action)) {
|
|
4383
|
+
throw new ValidationError(
|
|
4384
|
+
"action must be read | write | branch | notify | external"
|
|
4385
|
+
);
|
|
4386
|
+
}
|
|
4387
|
+
const nodeId = typeof input.node_id === "string" ? input.node_id : null;
|
|
4388
|
+
const edgeId = typeof input.edge_id === "string" ? input.edge_id : null;
|
|
4389
|
+
const detail = input.detail === void 0 ? null : JSON.stringify(input.detail);
|
|
4390
|
+
const db = getDb();
|
|
4391
|
+
let seq = 0;
|
|
4392
|
+
await db.transaction(async (tx) => {
|
|
4393
|
+
const cur = await tx.get("SELECT status FROM runs WHERE id = ?", [runId]);
|
|
4394
|
+
if (!cur) throw new NotFoundError("Run not found");
|
|
4395
|
+
if (cur.status !== "running") {
|
|
4396
|
+
throw new ConflictError(
|
|
4397
|
+
`Run is ${cur.status} \u2014 steps can't be appended`
|
|
4398
|
+
);
|
|
4399
|
+
}
|
|
4400
|
+
const last = await tx.get(
|
|
4401
|
+
"SELECT MAX(seq) as m FROM run_steps WHERE run_id = ?",
|
|
4402
|
+
[runId]
|
|
4403
|
+
);
|
|
4404
|
+
seq = (Number(last.m) || 0) + 1;
|
|
4405
|
+
if (seq > config.RUN_MAX_STEPS) {
|
|
4406
|
+
throw new ConflictError(
|
|
4407
|
+
`Run has reached the step limit of ${config.RUN_MAX_STEPS} \u2014 no more steps can be appended`
|
|
4408
|
+
);
|
|
4409
|
+
}
|
|
4410
|
+
await tx.run(
|
|
4411
|
+
`INSERT INTO run_steps (run_id, seq, node_id, edge_id, action, detail, at)
|
|
4412
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
4413
|
+
[runId, seq, nodeId, edgeId, action, detail, (/* @__PURE__ */ new Date()).toISOString()]
|
|
4414
|
+
);
|
|
4415
|
+
});
|
|
4416
|
+
return { seq };
|
|
4417
|
+
}
|
|
4418
|
+
var RUN_STATUSES = ["running", "succeeded", "failed", "cancelled"];
|
|
4419
|
+
async function closeRun(runId, input) {
|
|
4420
|
+
const status = input.status;
|
|
4421
|
+
if (typeof status !== "string" || !RUN_STATUSES.includes(status)) {
|
|
4422
|
+
throw new ValidationError("status must be running | succeeded | failed | cancelled");
|
|
4423
|
+
}
|
|
4424
|
+
const terminal = status !== "running";
|
|
4425
|
+
const res = await getDb().run(
|
|
4426
|
+
`UPDATE runs SET status = ?, trace = COALESCE(?, trace), finished_at = ?
|
|
4427
|
+
WHERE id = ? AND status = 'running'`,
|
|
4428
|
+
[
|
|
4429
|
+
status,
|
|
4430
|
+
input.trace === void 0 ? null : JSON.stringify(input.trace),
|
|
4431
|
+
terminal ? (/* @__PURE__ */ new Date()).toISOString() : null,
|
|
4432
|
+
runId
|
|
4433
|
+
]
|
|
4434
|
+
);
|
|
4435
|
+
if (res.changes === 0) {
|
|
4436
|
+
const run = await loadRun(runId);
|
|
4437
|
+
if (!run) throw new NotFoundError("Run not found");
|
|
4438
|
+
throw new ConflictError("Run already terminal \u2014 status can't be changed");
|
|
4439
|
+
}
|
|
4440
|
+
return { status };
|
|
4441
|
+
}
|
|
4442
|
+
async function getRunTrace(run, opts) {
|
|
4443
|
+
const { canSeeDetail } = opts;
|
|
4444
|
+
const steps = await getDb().all(
|
|
4445
|
+
"SELECT seq, node_id, edge_id, action, detail, at FROM run_steps WHERE run_id = ? ORDER BY seq",
|
|
4446
|
+
[run.id]
|
|
4447
|
+
);
|
|
4448
|
+
return {
|
|
4449
|
+
run: {
|
|
4450
|
+
...run,
|
|
4451
|
+
inputs: canSeeDetail ? safeJson(run.inputs) : null,
|
|
4452
|
+
trace: canSeeDetail ? safeJson(run.trace) : null
|
|
4453
|
+
},
|
|
4454
|
+
steps: steps.map((s) => ({
|
|
4455
|
+
...s,
|
|
4456
|
+
detail: canSeeDetail ? safeJson(s.detail) : null
|
|
4457
|
+
}))
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
async function listRuns(nestId, opts) {
|
|
4461
|
+
const db = getDb();
|
|
4462
|
+
let sql = "SELECT * FROM runs WHERE nest_id = ?";
|
|
4463
|
+
const args = [nestId];
|
|
4464
|
+
if (opts.agent) {
|
|
4465
|
+
sql += " AND agent_node = ?";
|
|
4466
|
+
args.push(opts.agent);
|
|
4467
|
+
}
|
|
4468
|
+
if (opts.status) {
|
|
4469
|
+
sql += " AND status = ?";
|
|
4470
|
+
args.push(opts.status);
|
|
4471
|
+
}
|
|
4472
|
+
sql += " ORDER BY started_at DESC LIMIT ?";
|
|
4473
|
+
args.push(Math.min(Math.max(Number(opts.limit) || 50, 1), 500));
|
|
4474
|
+
const rows = await db.all(sql, args);
|
|
4475
|
+
return {
|
|
4476
|
+
count: rows.length,
|
|
4477
|
+
runs: rows.map((r) => ({
|
|
4478
|
+
...r,
|
|
4479
|
+
inputs: opts.canSeeDetail ? safeJson(r.inputs) : null,
|
|
4480
|
+
trace: opts.canSeeDetail ? safeJson(r.trace) : null
|
|
4481
|
+
}))
|
|
4482
|
+
};
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
// src/workflow/edge-routes.ts
|
|
4486
|
+
import { Hono as Hono9 } from "hono";
|
|
4487
|
+
import { v4 as uuid7 } from "uuid";
|
|
4488
|
+
|
|
4489
|
+
// src/workflow/edge-type-routes.ts
|
|
4490
|
+
import { Hono as Hono8 } from "hono";
|
|
4491
|
+
import { v4 as uuid6 } from "uuid";
|
|
4492
|
+
var requireWorkflowPlane = async (c, next) => {
|
|
4493
|
+
if (!config.FEATURE_WORKFLOW_PLANE) {
|
|
4494
|
+
return c.json(
|
|
4495
|
+
{
|
|
4496
|
+
error: "The workflow plane is not enabled on this server. An admin can turn it on with FEATURE_WORKFLOW_PLANE=true."
|
|
4497
|
+
},
|
|
4498
|
+
404
|
|
4499
|
+
);
|
|
4500
|
+
}
|
|
4501
|
+
return next();
|
|
4502
|
+
};
|
|
4503
|
+
async function seedDefaultEdgeTypes(nestId) {
|
|
4504
|
+
const db = getDb();
|
|
4505
|
+
const existing = await db.get(
|
|
4506
|
+
"SELECT COUNT(*) as c FROM edge_types WHERE nest_id = ?",
|
|
4507
|
+
[nestId]
|
|
4508
|
+
);
|
|
4509
|
+
if (existing.c > 0) return;
|
|
4510
|
+
const owner = await db.get(
|
|
4511
|
+
"SELECT u.email FROM nests n JOIN users u ON u.id = n.user_id WHERE n.id = ?",
|
|
4512
|
+
[nestId]
|
|
4513
|
+
);
|
|
4514
|
+
const createdBy = owner?.email ?? "admin@localhost";
|
|
4515
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4516
|
+
const defaults = [
|
|
4517
|
+
["next", "Unconditional flow: after the source completes, run the target.", 1, "#16a34a"],
|
|
4518
|
+
["on-success", "Follow only when the source step succeeded.", 1, "#16a34a"],
|
|
4519
|
+
["on-failure", "Follow only when the source step failed.", 1, "#ef4444"],
|
|
4520
|
+
["depends-on", "The source requires the target (ordering/lineage; not conditional flow).", 0, "#3b82f6"],
|
|
4521
|
+
["owned-by", "Ownership: the target person/team is accountable for the source node.", 0, "#64748b"]
|
|
4522
|
+
];
|
|
4523
|
+
for (const [name, description, isFlow, color] of defaults) {
|
|
4524
|
+
await db.run(
|
|
4525
|
+
`INSERT INTO edge_types
|
|
4526
|
+
(id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
|
|
4527
|
+
VALUES (?, ?, ?, ?, 'directed', ?, NULL, ?, ?, ?, ?)
|
|
4528
|
+
ON CONFLICT DO NOTHING`,
|
|
4529
|
+
[uuid6(), nestId, name, description, isFlow, color, createdBy, now, now]
|
|
4530
|
+
);
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
function parseConditionSchema(raw) {
|
|
4534
|
+
if (raw === void 0 || raw === null || raw === "") return null;
|
|
4535
|
+
const obj = typeof raw === "string" ? safeJson(raw) : raw;
|
|
4536
|
+
if (!obj || typeof obj !== "object" || !Array.isArray(obj.params) || !obj.params.every((p) => typeof p === "string")) {
|
|
4537
|
+
throw new ValidationError(
|
|
4538
|
+
'condition_schema must be {"params": [string\u2026], "mode_default"?: "structured"|"nl"|"open"}'
|
|
4539
|
+
);
|
|
4540
|
+
}
|
|
4541
|
+
const mode = obj.mode_default;
|
|
4542
|
+
if (mode !== void 0 && !["structured", "nl", "open"].includes(mode)) {
|
|
4543
|
+
throw new ValidationError(
|
|
4544
|
+
"condition_schema.mode_default must be structured | nl | open"
|
|
4545
|
+
);
|
|
4546
|
+
}
|
|
4547
|
+
return JSON.stringify({ params: obj.params, mode_default: mode ?? "structured" });
|
|
4548
|
+
}
|
|
4549
|
+
var edgeTypeToResponse = (r) => ({
|
|
4550
|
+
...r,
|
|
4551
|
+
is_flow: !!r.is_flow,
|
|
4552
|
+
condition_schema: r.condition_schema ? safeJson(r.condition_schema) : null
|
|
4553
|
+
});
|
|
4554
|
+
var edgeTypeRoutes = new Hono8();
|
|
4555
|
+
edgeTypeRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
4556
|
+
const nestId = c.req.param("nestId");
|
|
4557
|
+
await seedDefaultEdgeTypes(nestId);
|
|
4558
|
+
const rows = await getDb().all(
|
|
4559
|
+
"SELECT * FROM edge_types WHERE nest_id = ? ORDER BY LOWER(name)",
|
|
4560
|
+
[nestId]
|
|
4561
|
+
);
|
|
4562
|
+
return c.json({ count: rows.length, edge_types: rows.map(edgeTypeToResponse) });
|
|
4563
|
+
});
|
|
4564
|
+
edgeTypeRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
4565
|
+
const nestId = c.req.param("nestId");
|
|
4566
|
+
const body = await c.req.json();
|
|
4567
|
+
if (typeof body.name !== "string" || !body.name.trim()) {
|
|
4568
|
+
throw new ValidationError("name is required");
|
|
4569
|
+
}
|
|
4570
|
+
if (typeof body.description !== "string" || !body.description.trim()) {
|
|
4571
|
+
throw new ValidationError("description (the articulation) is required");
|
|
4572
|
+
}
|
|
4573
|
+
const name = body.name.trim();
|
|
4574
|
+
if (name.length > 100 || !/^[a-z0-9][a-z0-9-]*$/i.test(name)) {
|
|
4575
|
+
throw new ValidationError(
|
|
4576
|
+
"name must be alphanumeric-with-dashes, 100 chars max (e.g. escalates-when)"
|
|
4577
|
+
);
|
|
4578
|
+
}
|
|
4579
|
+
const direction = String(body.direction ?? "directed");
|
|
4580
|
+
if (!["directed", "undirected"].includes(direction)) {
|
|
4581
|
+
throw new ValidationError("direction must be directed | undirected");
|
|
4582
|
+
}
|
|
4583
|
+
const conditionSchema = parseConditionSchema(body.condition_schema);
|
|
4584
|
+
const isFlow = body.is_flow === true ? 1 : 0;
|
|
4585
|
+
let color = null;
|
|
4586
|
+
if (body.color !== void 0 && body.color !== null && body.color !== "") {
|
|
4587
|
+
if (typeof body.color !== "string" || !/^#[0-9a-f]{6}$/i.test(body.color)) {
|
|
4588
|
+
throw new ValidationError("color must be a 6-digit hex, e.g. #16a34a");
|
|
4589
|
+
}
|
|
4590
|
+
color = body.color;
|
|
4591
|
+
}
|
|
4592
|
+
const db = getDb();
|
|
4593
|
+
const definedBy = await resolveCallerEmail(c.get("userId"));
|
|
4594
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4595
|
+
await seedDefaultEdgeTypes(nestId);
|
|
4596
|
+
let created = false;
|
|
4597
|
+
await db.transaction(async (tx) => {
|
|
4598
|
+
const existing = await tx.get(
|
|
4599
|
+
"SELECT id FROM edge_types WHERE nest_id = ? AND LOWER(name) = LOWER(?)",
|
|
4600
|
+
[nestId, name]
|
|
4601
|
+
);
|
|
4602
|
+
if (existing) {
|
|
4603
|
+
await tx.run(
|
|
4604
|
+
`UPDATE edge_types SET name = ?, description = ?, direction = ?, is_flow = ?,
|
|
4605
|
+
condition_schema = ?, color = ?, updated_at = ? WHERE id = ?`,
|
|
4606
|
+
[name, body.description.trim(), direction, isFlow, conditionSchema, color, now, existing.id]
|
|
4607
|
+
);
|
|
4608
|
+
} else {
|
|
4609
|
+
created = true;
|
|
4610
|
+
await tx.run(
|
|
4611
|
+
`INSERT INTO edge_types
|
|
4612
|
+
(id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
|
|
4613
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4614
|
+
[uuid6(), nestId, name, body.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
|
|
4615
|
+
);
|
|
4616
|
+
}
|
|
4617
|
+
});
|
|
4618
|
+
const row = await db.get(
|
|
4619
|
+
"SELECT * FROM edge_types WHERE nest_id = ? AND LOWER(name) = LOWER(?)",
|
|
4620
|
+
[nestId, name]
|
|
4621
|
+
);
|
|
4622
|
+
return c.json({ edge_type: edgeTypeToResponse(row) }, created ? 201 : 200);
|
|
4623
|
+
});
|
|
4624
|
+
edgeTypeRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
|
|
4625
|
+
const nestId = c.req.param("nestId");
|
|
4626
|
+
const id = c.req.param("id");
|
|
4627
|
+
await getDb().transaction(async (tx) => {
|
|
4628
|
+
const row = await tx.get(
|
|
4629
|
+
"SELECT id FROM edge_types WHERE id = ? AND nest_id = ?",
|
|
4630
|
+
[id, nestId]
|
|
4631
|
+
);
|
|
4632
|
+
if (!row) throw new NotFoundError("Edge type not found");
|
|
4633
|
+
const inUse = await tx.get(
|
|
4634
|
+
"SELECT COUNT(*) as c FROM edges WHERE type_id = ?",
|
|
4635
|
+
[id]
|
|
4636
|
+
);
|
|
4637
|
+
if (inUse.c > 0) {
|
|
4638
|
+
throw new ValidationError(
|
|
4639
|
+
`Edge type is in use by ${inUse.c} edge${inUse.c === 1 ? "" : "s"} \u2014 delete those first.`
|
|
4640
|
+
);
|
|
4641
|
+
}
|
|
4642
|
+
await tx.run("DELETE FROM edge_types WHERE id = ?", [id]);
|
|
4643
|
+
});
|
|
4644
|
+
return c.json({ deleted: true });
|
|
4645
|
+
});
|
|
4646
|
+
|
|
4647
|
+
// src/workflow/edge-routes.ts
|
|
4648
|
+
function toResponse(e, t) {
|
|
4649
|
+
return {
|
|
4650
|
+
id: e.id,
|
|
4651
|
+
from_node: e.from_node,
|
|
4652
|
+
to_node: e.to_node,
|
|
4653
|
+
type: { id: t.id, name: t.name, color: t.color, is_flow: !!t.is_flow },
|
|
4654
|
+
condition_mode: e.condition_mode,
|
|
4655
|
+
condition: e.condition_mode === "structured" ? safeJson(e.condition) : e.condition,
|
|
4656
|
+
metadata: safeJson(e.metadata),
|
|
4657
|
+
created_by: e.created_by,
|
|
4658
|
+
created_at: e.created_at,
|
|
4659
|
+
updated_at: e.updated_at
|
|
4660
|
+
};
|
|
4661
|
+
}
|
|
4662
|
+
async function validateCondition(nestId, mode, condition) {
|
|
4663
|
+
if (mode === void 0 || mode === null) {
|
|
4664
|
+
if (condition !== void 0 && condition !== null) {
|
|
4665
|
+
throw new ValidationError("condition requires condition_mode");
|
|
4666
|
+
}
|
|
4667
|
+
return { mode: null, stored: null };
|
|
4668
|
+
}
|
|
4669
|
+
if (!["structured", "nl", "open"].includes(mode)) {
|
|
4670
|
+
throw new ValidationError("condition_mode must be structured | nl | open");
|
|
4671
|
+
}
|
|
4672
|
+
if (condition === void 0 || condition === null || condition === "") {
|
|
4673
|
+
throw new ValidationError(`condition_mode "${mode}" requires a condition`);
|
|
4674
|
+
}
|
|
4675
|
+
if (mode === "structured") {
|
|
4676
|
+
if (typeof condition !== "object" || Array.isArray(condition)) {
|
|
4677
|
+
throw new ValidationError(
|
|
4678
|
+
'structured condition must be an object, e.g. {"term":"CAC","op":">","value":500}'
|
|
4679
|
+
);
|
|
4680
|
+
}
|
|
4681
|
+
const c = condition;
|
|
4682
|
+
if (typeof c.term === "string" && c.term.trim()) {
|
|
4683
|
+
try {
|
|
4684
|
+
const row = await getDb().get(
|
|
4685
|
+
"SELECT id FROM definitions WHERE nest_id = ? AND LOWER(term) = LOWER(?)",
|
|
4686
|
+
[nestId, c.term.trim()]
|
|
4687
|
+
);
|
|
4688
|
+
if (!row) {
|
|
4689
|
+
throw new ValidationError(
|
|
4690
|
+
`structured condition references undefined term "${c.term}" \u2014 define it in the glossary first`
|
|
4691
|
+
);
|
|
4692
|
+
}
|
|
4693
|
+
} catch (err) {
|
|
4694
|
+
if (err instanceof ValidationError) throw err;
|
|
4695
|
+
if (!/no such table/i.test(err?.message ?? "")) throw err;
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
return { mode, stored: JSON.stringify(condition) };
|
|
4699
|
+
}
|
|
4700
|
+
if (typeof condition !== "string") {
|
|
4701
|
+
throw new ValidationError(`condition for mode "${mode}" must be a string`);
|
|
4702
|
+
}
|
|
4703
|
+
return { mode, stored: condition };
|
|
4704
|
+
}
|
|
4705
|
+
async function assertNoFlowCycle(db, nestId, fromNode, toNode) {
|
|
4706
|
+
if (fromNode === toNode) {
|
|
4707
|
+
throw new ConflictError(
|
|
4708
|
+
`cycle: ["${fromNode}","${toNode}"] \u2014 flow edges must form a DAG`
|
|
4709
|
+
);
|
|
4710
|
+
}
|
|
4711
|
+
const rows = await db.all(
|
|
4712
|
+
`SELECT e.from_node, e.to_node FROM edges e
|
|
4713
|
+
JOIN edge_types t ON t.id = e.type_id
|
|
4714
|
+
WHERE e.nest_id = ? AND t.is_flow = 1`,
|
|
4715
|
+
[nestId]
|
|
4716
|
+
);
|
|
4717
|
+
const adj = /* @__PURE__ */ new Map();
|
|
4718
|
+
for (const r of rows) {
|
|
4719
|
+
(adj.get(r.from_node) ?? adj.set(r.from_node, []).get(r.from_node)).push(
|
|
4720
|
+
r.to_node
|
|
4721
|
+
);
|
|
4722
|
+
}
|
|
4723
|
+
const parent = /* @__PURE__ */ new Map();
|
|
4724
|
+
const queue = [toNode];
|
|
4725
|
+
const seen = /* @__PURE__ */ new Set([toNode]);
|
|
4726
|
+
while (queue.length) {
|
|
4727
|
+
const cur = queue.shift();
|
|
4728
|
+
if (cur === fromNode) {
|
|
4729
|
+
const path = [fromNode];
|
|
4730
|
+
let p = fromNode;
|
|
4731
|
+
while (p !== toNode) {
|
|
4732
|
+
p = parent.get(p);
|
|
4733
|
+
path.push(p);
|
|
4734
|
+
}
|
|
4735
|
+
path.reverse().push(toNode);
|
|
4736
|
+
throw new ConflictError(
|
|
4737
|
+
`cycle: ${JSON.stringify(path)} \u2014 flow edges must form a DAG`
|
|
4738
|
+
);
|
|
4739
|
+
}
|
|
4740
|
+
for (const nb of adj.get(cur) ?? []) {
|
|
4741
|
+
if (!seen.has(nb)) {
|
|
4742
|
+
seen.add(nb);
|
|
4743
|
+
parent.set(nb, cur);
|
|
4744
|
+
queue.push(nb);
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
var edgeRoutes = new Hono9();
|
|
4750
|
+
edgeRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
4751
|
+
const nestId = c.req.param("nestId");
|
|
4752
|
+
const db = getDb();
|
|
4753
|
+
const typeFilter = c.req.query("type")?.trim();
|
|
4754
|
+
const nodeFilter = c.req.query("node")?.trim();
|
|
4755
|
+
let sql = `SELECT e.*, t.id as t_id, t.name as t_name, t.color as t_color, t.is_flow as t_is_flow
|
|
4756
|
+
FROM edges e JOIN edge_types t ON t.id = e.type_id
|
|
4757
|
+
WHERE e.nest_id = ?`;
|
|
4758
|
+
const args = [nestId];
|
|
4759
|
+
if (typeFilter) {
|
|
4760
|
+
sql += " AND (t.id = ? OR LOWER(t.name) = LOWER(?))";
|
|
4761
|
+
args.push(typeFilter, typeFilter);
|
|
4762
|
+
}
|
|
4763
|
+
if (nodeFilter) {
|
|
4764
|
+
sql += " AND (e.from_node = ? OR e.to_node = ?)";
|
|
4765
|
+
args.push(nodeFilter, nodeFilter);
|
|
4766
|
+
}
|
|
4767
|
+
sql += " ORDER BY e.created_at";
|
|
4768
|
+
const rows = await db.all(sql, args);
|
|
4769
|
+
const edges = rows.map(
|
|
4770
|
+
(r) => toResponse(r, {
|
|
4771
|
+
id: r.t_id,
|
|
4772
|
+
name: r.t_name,
|
|
4773
|
+
color: r.t_color,
|
|
4774
|
+
is_flow: r.t_is_flow
|
|
4775
|
+
})
|
|
4776
|
+
);
|
|
4777
|
+
return c.json({ count: edges.length, edges });
|
|
4778
|
+
});
|
|
4779
|
+
edgeRoutes.post("/", requireWorkflowPlane, async (c) => {
|
|
4780
|
+
const nestId = c.req.param("nestId");
|
|
4781
|
+
const body = await c.req.json();
|
|
4782
|
+
const fromRaw = typeof body.from_node === "string" ? body.from_node.trim() : "";
|
|
4783
|
+
const toRaw = typeof body.to_node === "string" ? body.to_node.trim() : "";
|
|
4784
|
+
const typeRef = typeof body.type === "string" ? body.type.trim() : "";
|
|
4785
|
+
if (!fromRaw || !toRaw || !typeRef) {
|
|
4786
|
+
throw new ValidationError("from_node, to_node, and type are required");
|
|
4787
|
+
}
|
|
4788
|
+
const fromNode = assertSafeNodeId(fromRaw);
|
|
4789
|
+
const toNode = assertSafeNodeId(toRaw);
|
|
4790
|
+
const db = getDb();
|
|
4791
|
+
const createdBy = await resolveCallerEmail(c.get("userId"));
|
|
4792
|
+
await seedDefaultEdgeTypes(nestId);
|
|
4793
|
+
const type = await db.get(
|
|
4794
|
+
"SELECT * FROM edge_types WHERE nest_id = ? AND (id = ? OR LOWER(name) = LOWER(?))",
|
|
4795
|
+
[nestId, typeRef, typeRef]
|
|
4796
|
+
);
|
|
4797
|
+
if (!type) {
|
|
4798
|
+
throw new ValidationError(
|
|
4799
|
+
`unknown edge type "${typeRef}" \u2014 define it in the registry first`
|
|
4800
|
+
);
|
|
4801
|
+
}
|
|
4802
|
+
const { storage } = await engineCache.get(nestId);
|
|
4803
|
+
for (const nodeId of [fromNode, toNode]) {
|
|
4804
|
+
try {
|
|
4805
|
+
await storage.readDocument(nodeId);
|
|
4806
|
+
} catch {
|
|
4807
|
+
throw new ValidationError(`node not found: "${nodeId}"`);
|
|
4808
|
+
}
|
|
4809
|
+
}
|
|
4810
|
+
const { mode, stored } = await validateCondition(
|
|
4811
|
+
nestId,
|
|
4812
|
+
body.condition_mode,
|
|
4813
|
+
body.condition
|
|
4814
|
+
);
|
|
4815
|
+
const metadata = body.metadata === void 0 || body.metadata === null ? null : JSON.stringify(body.metadata);
|
|
4816
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4817
|
+
const id = uuid7();
|
|
4818
|
+
await db.transaction(async (tx) => {
|
|
4819
|
+
if (type.is_flow) {
|
|
4820
|
+
await assertNoFlowCycle(tx, nestId, fromNode, toNode);
|
|
4821
|
+
}
|
|
4822
|
+
await tx.run(
|
|
4823
|
+
`INSERT INTO edges
|
|
4824
|
+
(id, nest_id, from_node, to_node, type_id, condition_mode, condition, metadata, created_by, created_at, updated_at)
|
|
4825
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
4826
|
+
[id, nestId, fromNode, toNode, type.id, mode, stored, metadata, createdBy, now, now]
|
|
4827
|
+
);
|
|
4828
|
+
});
|
|
4829
|
+
const row = await db.get("SELECT * FROM edges WHERE id = ?", [id]);
|
|
4830
|
+
return c.json({ edge: toResponse(row, type) }, 201);
|
|
4831
|
+
});
|
|
4832
|
+
edgeRoutes.delete("/:id", requireWorkflowPlane, async (c) => {
|
|
4833
|
+
const nestId = c.req.param("nestId");
|
|
4834
|
+
const id = c.req.param("id");
|
|
4835
|
+
const db = getDb();
|
|
4836
|
+
const row = await db.get(
|
|
4837
|
+
"SELECT id FROM edges WHERE id = ? AND nest_id = ?",
|
|
4838
|
+
[id, nestId]
|
|
4839
|
+
);
|
|
4840
|
+
if (!row) throw new NotFoundError("Edge not found");
|
|
4841
|
+
await db.run("DELETE FROM edges WHERE id = ?", [id]);
|
|
4842
|
+
return c.json({ deleted: true });
|
|
4843
|
+
});
|
|
4844
|
+
|
|
4845
|
+
// src/mcp/workflow-tools.ts
|
|
4846
|
+
var WORKFLOW_TOOL_DEFINITIONS = [
|
|
4847
|
+
// ── Execute ──────────────────────────────────────────────────────────
|
|
4848
|
+
{
|
|
4849
|
+
name: "workflow_run",
|
|
4850
|
+
description: "Trigger a governed workflow run for an agent/skill node and get the executable bundle back (agent instructions, the reachable flow-edge subgraph, and referenced glossary definitions) plus a run_id. The server does not execute \u2014 YOU are the runner: walk the edges, then report progress with workflow_run_step and finish with workflow_run_close.",
|
|
4851
|
+
inputSchema: {
|
|
4852
|
+
type: "object",
|
|
4853
|
+
properties: {
|
|
4854
|
+
agent: { type: "string", description: "Node id of the agent/skill to run" },
|
|
4855
|
+
inputs: { type: "object", description: "Optional caller inputs recorded on the run" },
|
|
4856
|
+
parent_run_id: {
|
|
4857
|
+
type: "string",
|
|
4858
|
+
description: "Optional: spawn this as a sub-agent run under a still-running parent (FEATURE_SUBAGENT_RUNS). Bounded by depth/fan-out and guarded against re-entrancy."
|
|
4859
|
+
}
|
|
4860
|
+
},
|
|
4861
|
+
required: ["agent"]
|
|
4862
|
+
}
|
|
4863
|
+
},
|
|
4864
|
+
{
|
|
4865
|
+
name: "workflow_run_step",
|
|
4866
|
+
description: "Append one step to a run's trace after every action. seq is assigned server-side. For branch steps put the evaluated condition, result and (NL mode) rationale in detail. Fails if the run is already closed.",
|
|
4867
|
+
inputSchema: {
|
|
4868
|
+
type: "object",
|
|
4869
|
+
properties: {
|
|
4870
|
+
run_id: { type: "string" },
|
|
4871
|
+
action: { type: "string", description: "read | write | branch | notify | external" },
|
|
4872
|
+
node_id: { type: "string", description: "Node the step touched (optional)" },
|
|
4873
|
+
edge_id: { type: "string", description: "Edge the step followed (optional)" },
|
|
4874
|
+
detail: { type: "object", description: "Freeform payload: reads, condition results, rationale" }
|
|
4875
|
+
},
|
|
4876
|
+
required: ["run_id", "action"]
|
|
4877
|
+
}
|
|
4878
|
+
},
|
|
4879
|
+
{
|
|
4880
|
+
name: "workflow_run_close",
|
|
4881
|
+
description: "Close a run with a terminal status (succeeded | failed | cancelled) and an optional trace summary. Terminal is final \u2014 a second close returns a conflict.",
|
|
4882
|
+
inputSchema: {
|
|
4883
|
+
type: "object",
|
|
4884
|
+
properties: {
|
|
4885
|
+
run_id: { type: "string" },
|
|
4886
|
+
status: { type: "string", description: "succeeded | failed | cancelled (or running to update trace)" },
|
|
4887
|
+
trace: { type: "object", description: "Optional summary/totals recorded on the run" }
|
|
4888
|
+
},
|
|
4889
|
+
required: ["run_id", "status"]
|
|
4890
|
+
}
|
|
4891
|
+
},
|
|
4892
|
+
{
|
|
4893
|
+
name: "workflow_run_get",
|
|
4894
|
+
description: "Fetch one run's full trace (steps in order). Freeform payloads (inputs, trace, step detail) are redacted unless you have write access to the nest.",
|
|
4895
|
+
inputSchema: {
|
|
4896
|
+
type: "object",
|
|
4897
|
+
properties: { run_id: { type: "string" } },
|
|
4898
|
+
required: ["run_id"]
|
|
4899
|
+
}
|
|
4900
|
+
},
|
|
4901
|
+
{
|
|
4902
|
+
name: "workflow_runs_list",
|
|
4903
|
+
description: "List runs in this nest, most recent first. Filter by agent node id and/or status (running|succeeded|failed|cancelled).",
|
|
4904
|
+
inputSchema: {
|
|
4905
|
+
type: "object",
|
|
4906
|
+
properties: {
|
|
4907
|
+
agent: { type: "string" },
|
|
4908
|
+
status: { type: "string" },
|
|
4909
|
+
limit: { type: "number" }
|
|
4910
|
+
},
|
|
4911
|
+
required: []
|
|
4912
|
+
}
|
|
4913
|
+
},
|
|
4914
|
+
// ── Build: edges ─────────────────────────────────────────────────────
|
|
4915
|
+
{
|
|
4916
|
+
name: "workflow_edges_list",
|
|
4917
|
+
description: "List typed edges in this nest. Optional filters: type (edge-type name or id) and node (id appearing as either endpoint).",
|
|
4918
|
+
inputSchema: {
|
|
4919
|
+
type: "object",
|
|
4920
|
+
properties: { type: { type: "string" }, node: { type: "string" } },
|
|
4921
|
+
required: []
|
|
4922
|
+
}
|
|
4923
|
+
},
|
|
4924
|
+
{
|
|
4925
|
+
name: "workflow_edge_create",
|
|
4926
|
+
description: "Create a typed edge between two existing nodes. The edge type must already exist in the registry. Optional condition in mode structured (JSON predicate {term,op,value}; term must be a defined glossary term), nl (natural-language, judged at run time), or open. Flow-type edges must keep the graph a DAG \u2014 a cycle is rejected.",
|
|
4927
|
+
inputSchema: {
|
|
4928
|
+
type: "object",
|
|
4929
|
+
properties: {
|
|
4930
|
+
from_node: { type: "string" },
|
|
4931
|
+
to_node: { type: "string" },
|
|
4932
|
+
type: { type: "string", description: "Edge-type name or id" },
|
|
4933
|
+
condition_mode: { type: "string", description: "structured | nl | open" },
|
|
4934
|
+
condition: { type: "object", description: "Predicate object (structured) or string (nl/open)" },
|
|
4935
|
+
metadata: { type: "object" }
|
|
4936
|
+
},
|
|
4937
|
+
required: ["from_node", "to_node", "type"]
|
|
4938
|
+
}
|
|
4939
|
+
},
|
|
4940
|
+
{
|
|
4941
|
+
name: "workflow_edge_delete",
|
|
4942
|
+
description: "Delete an edge by id.",
|
|
4943
|
+
inputSchema: {
|
|
4944
|
+
type: "object",
|
|
4945
|
+
properties: { edge_id: { type: "string" } },
|
|
4946
|
+
required: ["edge_id"]
|
|
4947
|
+
}
|
|
4948
|
+
},
|
|
4949
|
+
// ── Build: edge types ────────────────────────────────────────────────
|
|
4950
|
+
{
|
|
4951
|
+
name: "workflow_edge_types_list",
|
|
4952
|
+
description: "List this nest's edge-type registry \u2014 the articulated vocabulary of relations. Seeds the five stock flow types on first use.",
|
|
4953
|
+
inputSchema: { type: "object", properties: {}, required: [] }
|
|
4954
|
+
},
|
|
4955
|
+
{
|
|
4956
|
+
name: "workflow_edge_type_upsert",
|
|
4957
|
+
description: "Create or update an edge type by name (case-insensitive). name is a slug (alphanumeric-with-dashes). Set is_flow true for control-flow relations (they get DAG-validated). condition_schema is an optional {params:[string\u2026], mode_default?} template.",
|
|
4958
|
+
inputSchema: {
|
|
4959
|
+
type: "object",
|
|
4960
|
+
properties: {
|
|
4961
|
+
name: { type: "string" },
|
|
4962
|
+
description: { type: "string" },
|
|
4963
|
+
direction: { type: "string", description: "directed | undirected (default directed)" },
|
|
4964
|
+
is_flow: { type: "boolean" },
|
|
4965
|
+
condition_schema: { type: "object" },
|
|
4966
|
+
color: { type: "string", description: "6-digit hex, e.g. #16a34a" }
|
|
4967
|
+
},
|
|
4968
|
+
required: ["name", "description"]
|
|
4969
|
+
}
|
|
4970
|
+
},
|
|
4971
|
+
{
|
|
4972
|
+
name: "workflow_edge_type_delete",
|
|
4973
|
+
description: "Delete an edge type by id. Refused while any edge still references it.",
|
|
4974
|
+
inputSchema: {
|
|
4975
|
+
type: "object",
|
|
4976
|
+
properties: { edge_type_id: { type: "string" } },
|
|
4977
|
+
required: ["edge_type_id"]
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
];
|
|
4981
|
+
var WORKFLOW_TOOL_NAMES = new Set(WORKFLOW_TOOL_DEFINITIONS.map((t) => t.name));
|
|
4982
|
+
var isWorkflowTool = (name) => WORKFLOW_TOOL_NAMES.has(name);
|
|
4983
|
+
async function nestPermission(ctx) {
|
|
4984
|
+
if (config.AUTH_MODE === "open") return "owner";
|
|
4985
|
+
return resolveNestPermission(ctx.nestId, ctx.userId);
|
|
4986
|
+
}
|
|
4987
|
+
function requireTier(perm, tier) {
|
|
4988
|
+
if (permissionLevel(perm) < permissionLevel(tier)) {
|
|
4989
|
+
throw new ForbiddenError(
|
|
4990
|
+
`This action needs ${tier} access to the nest.`
|
|
4991
|
+
);
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
async function loadNestRun(ctx, runId) {
|
|
4995
|
+
if (typeof runId !== "string" || !runId) {
|
|
4996
|
+
throw new ValidationError("run_id is required");
|
|
4997
|
+
}
|
|
4998
|
+
const run = await loadRun(runId);
|
|
4999
|
+
if (!run || run.nest_id !== ctx.nestId) throw new NotFoundError("Run not found");
|
|
5000
|
+
return run;
|
|
5001
|
+
}
|
|
5002
|
+
var json = (v) => JSON.stringify(v, null, 2);
|
|
5003
|
+
async function runWorkflowTool(toolName, args, ctx) {
|
|
5004
|
+
const perm = await nestPermission(ctx);
|
|
5005
|
+
switch (toolName) {
|
|
5006
|
+
// ── Execute ────────────────────────────────────────────────────────
|
|
5007
|
+
case "workflow_run": {
|
|
5008
|
+
requireTier(perm, "write");
|
|
5009
|
+
const triggeredBy = await resolveCallerEmail(ctx.userId);
|
|
5010
|
+
const bundle = await triggerRun({
|
|
5011
|
+
nestId: ctx.nestId,
|
|
5012
|
+
agentNode: String(args.agent ?? ""),
|
|
5013
|
+
inputs: args.inputs,
|
|
5014
|
+
triggeredBy,
|
|
5015
|
+
storage: ctx.storage,
|
|
5016
|
+
parentRunId: args.parent_run_id
|
|
5017
|
+
});
|
|
5018
|
+
return json(bundle);
|
|
5019
|
+
}
|
|
5020
|
+
case "workflow_run_step": {
|
|
5021
|
+
requireTier(perm, "write");
|
|
5022
|
+
const run = await loadNestRun(ctx, args.run_id);
|
|
5023
|
+
const { seq } = await appendRunStep(run.id, {
|
|
5024
|
+
action: args.action,
|
|
5025
|
+
node_id: args.node_id,
|
|
5026
|
+
edge_id: args.edge_id,
|
|
5027
|
+
detail: args.detail
|
|
5028
|
+
});
|
|
5029
|
+
return json({ ok: true, run_id: run.id, seq });
|
|
5030
|
+
}
|
|
5031
|
+
case "workflow_run_close": {
|
|
5032
|
+
requireTier(perm, "write");
|
|
5033
|
+
const run = await loadNestRun(ctx, args.run_id);
|
|
5034
|
+
const { status } = await closeRun(run.id, {
|
|
5035
|
+
status: args.status,
|
|
5036
|
+
trace: args.trace
|
|
5037
|
+
});
|
|
5038
|
+
return json({ ok: true, run_id: run.id, status });
|
|
5039
|
+
}
|
|
5040
|
+
case "workflow_run_get": {
|
|
5041
|
+
requireTier(perm, "read");
|
|
5042
|
+
const run = await loadNestRun(ctx, args.run_id);
|
|
5043
|
+
const canSeeDetail = permissionLevel(perm) >= permissionLevel("write");
|
|
5044
|
+
return json(await getRunTrace(run, { canSeeDetail }));
|
|
5045
|
+
}
|
|
5046
|
+
case "workflow_runs_list": {
|
|
5047
|
+
requireTier(perm, "read");
|
|
5048
|
+
const canSeeDetail = permissionLevel(perm) >= permissionLevel("write");
|
|
5049
|
+
return json(
|
|
5050
|
+
await listRuns(ctx.nestId, {
|
|
5051
|
+
agent: typeof args.agent === "string" ? args.agent : void 0,
|
|
5052
|
+
status: typeof args.status === "string" ? args.status : void 0,
|
|
5053
|
+
limit: typeof args.limit === "number" ? args.limit : void 0,
|
|
5054
|
+
canSeeDetail
|
|
5055
|
+
})
|
|
5056
|
+
);
|
|
5057
|
+
}
|
|
5058
|
+
// ── Build: edges ───────────────────────────────────────────────────
|
|
5059
|
+
case "workflow_edges_list": {
|
|
5060
|
+
requireTier(perm, "read");
|
|
5061
|
+
return json(await listEdges(ctx.nestId, args.type, args.node));
|
|
5062
|
+
}
|
|
5063
|
+
case "workflow_edge_create": {
|
|
5064
|
+
requireTier(perm, "write");
|
|
5065
|
+
return json(await createEdge(ctx, args));
|
|
5066
|
+
}
|
|
5067
|
+
case "workflow_edge_delete": {
|
|
5068
|
+
requireTier(perm, "write");
|
|
5069
|
+
const id = String(args.edge_id ?? "");
|
|
5070
|
+
const row = await getDb().get(
|
|
5071
|
+
"SELECT id FROM edges WHERE id = ? AND nest_id = ?",
|
|
5072
|
+
[id, ctx.nestId]
|
|
5073
|
+
);
|
|
5074
|
+
if (!row) throw new NotFoundError("Edge not found");
|
|
5075
|
+
await getDb().run("DELETE FROM edges WHERE id = ?", [id]);
|
|
5076
|
+
return json({ deleted: true, edge_id: id });
|
|
5077
|
+
}
|
|
5078
|
+
// ── Build: edge types ──────────────────────────────────────────────
|
|
5079
|
+
case "workflow_edge_types_list": {
|
|
5080
|
+
requireTier(perm, "read");
|
|
5081
|
+
await seedDefaultEdgeTypes(ctx.nestId);
|
|
5082
|
+
const rows = await getDb().all(
|
|
5083
|
+
"SELECT * FROM edge_types WHERE nest_id = ? ORDER BY LOWER(name)",
|
|
5084
|
+
[ctx.nestId]
|
|
5085
|
+
);
|
|
5086
|
+
return json({ count: rows.length, edge_types: rows.map(edgeTypeToResponse) });
|
|
5087
|
+
}
|
|
5088
|
+
case "workflow_edge_type_upsert": {
|
|
5089
|
+
requireTier(perm, "write");
|
|
5090
|
+
return json(await upsertEdgeType(ctx, args));
|
|
5091
|
+
}
|
|
5092
|
+
case "workflow_edge_type_delete": {
|
|
5093
|
+
requireTier(perm, "write");
|
|
5094
|
+
return json(await deleteEdgeType(ctx.nestId, String(args.edge_type_id ?? "")));
|
|
5095
|
+
}
|
|
5096
|
+
default:
|
|
5097
|
+
return json({ error: `Unknown workflow tool: ${toolName}` });
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
async function listEdges(nestId, type, node) {
|
|
5101
|
+
const db = getDb();
|
|
5102
|
+
let sql = `SELECT e.*, t.id as t_id, t.name as t_name, t.color as t_color, t.is_flow as t_is_flow
|
|
5103
|
+
FROM edges e JOIN edge_types t ON t.id = e.type_id
|
|
5104
|
+
WHERE e.nest_id = ?`;
|
|
5105
|
+
const args = [nestId];
|
|
5106
|
+
if (typeof type === "string" && type.trim()) {
|
|
5107
|
+
sql += " AND (t.id = ? OR LOWER(t.name) = LOWER(?))";
|
|
5108
|
+
args.push(type.trim(), type.trim());
|
|
5109
|
+
}
|
|
5110
|
+
if (typeof node === "string" && node.trim()) {
|
|
5111
|
+
sql += " AND (e.from_node = ? OR e.to_node = ?)";
|
|
5112
|
+
args.push(node.trim(), node.trim());
|
|
5113
|
+
}
|
|
5114
|
+
sql += " ORDER BY e.created_at";
|
|
5115
|
+
const rows = await db.all(sql, args);
|
|
5116
|
+
const edges = rows.map(
|
|
5117
|
+
(r) => toResponse(r, {
|
|
5118
|
+
id: r.t_id,
|
|
5119
|
+
name: r.t_name,
|
|
5120
|
+
color: r.t_color,
|
|
5121
|
+
is_flow: r.t_is_flow
|
|
5122
|
+
})
|
|
5123
|
+
);
|
|
5124
|
+
return { count: edges.length, edges };
|
|
5125
|
+
}
|
|
5126
|
+
async function createEdge(ctx, args) {
|
|
5127
|
+
const fromNode = typeof args.from_node === "string" ? args.from_node.trim() : "";
|
|
5128
|
+
const toNode = typeof args.to_node === "string" ? args.to_node.trim() : "";
|
|
5129
|
+
const typeRef = typeof args.type === "string" ? args.type.trim() : "";
|
|
5130
|
+
if (!fromNode || !toNode || !typeRef) {
|
|
5131
|
+
throw new ValidationError("from_node, to_node, and type are required");
|
|
5132
|
+
}
|
|
5133
|
+
assertSafeNodeId(fromNode);
|
|
5134
|
+
assertSafeNodeId(toNode);
|
|
5135
|
+
const db = getDb();
|
|
5136
|
+
const createdBy = await resolveCallerEmail(ctx.userId);
|
|
5137
|
+
await seedDefaultEdgeTypes(ctx.nestId);
|
|
5138
|
+
const type = await db.get(
|
|
5139
|
+
"SELECT * FROM edge_types WHERE nest_id = ? AND (id = ? OR LOWER(name) = LOWER(?))",
|
|
5140
|
+
[ctx.nestId, typeRef, typeRef]
|
|
5141
|
+
);
|
|
5142
|
+
if (!type) {
|
|
5143
|
+
throw new ValidationError(
|
|
5144
|
+
`unknown edge type "${typeRef}" \u2014 create it first with workflow_edge_type_upsert`
|
|
5145
|
+
);
|
|
5146
|
+
}
|
|
5147
|
+
for (const nodeId of [fromNode, toNode]) {
|
|
5148
|
+
try {
|
|
5149
|
+
await ctx.storage.readDocument(nodeId);
|
|
5150
|
+
} catch {
|
|
5151
|
+
throw new ValidationError(`node not found: "${nodeId}"`);
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
const { mode, stored } = await validateCondition(
|
|
5155
|
+
ctx.nestId,
|
|
5156
|
+
args.condition_mode,
|
|
5157
|
+
args.condition
|
|
5158
|
+
);
|
|
5159
|
+
const metadata = args.metadata === void 0 || args.metadata === null ? null : JSON.stringify(args.metadata);
|
|
5160
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5161
|
+
const id = uuid8();
|
|
5162
|
+
await db.transaction(async (tx) => {
|
|
5163
|
+
if (type.is_flow) {
|
|
5164
|
+
await assertNoFlowCycle(tx, ctx.nestId, fromNode, toNode);
|
|
5165
|
+
}
|
|
5166
|
+
await tx.run(
|
|
5167
|
+
`INSERT INTO edges
|
|
5168
|
+
(id, nest_id, from_node, to_node, type_id, condition_mode, condition, metadata, created_by, created_at, updated_at)
|
|
5169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
5170
|
+
[id, ctx.nestId, fromNode, toNode, type.id, mode, stored, metadata, createdBy, now, now]
|
|
5171
|
+
);
|
|
5172
|
+
});
|
|
5173
|
+
const row = await db.get("SELECT * FROM edges WHERE id = ?", [id]);
|
|
5174
|
+
return { edge: toResponse(row, type) };
|
|
5175
|
+
}
|
|
5176
|
+
async function upsertEdgeType(ctx, args) {
|
|
5177
|
+
if (typeof args.name !== "string" || !args.name.trim()) {
|
|
5178
|
+
throw new ValidationError("name is required");
|
|
5179
|
+
}
|
|
5180
|
+
if (typeof args.description !== "string" || !args.description.trim()) {
|
|
5181
|
+
throw new ValidationError("description (the articulation) is required");
|
|
5182
|
+
}
|
|
5183
|
+
const name = args.name.trim();
|
|
5184
|
+
if (name.length > 100 || !/^[a-z0-9][a-z0-9-]*$/i.test(name)) {
|
|
5185
|
+
throw new ValidationError(
|
|
5186
|
+
"name must be alphanumeric-with-dashes, 100 chars max (e.g. escalates-when)"
|
|
5187
|
+
);
|
|
5188
|
+
}
|
|
5189
|
+
const direction = String(args.direction ?? "directed");
|
|
5190
|
+
if (!["directed", "undirected"].includes(direction)) {
|
|
5191
|
+
throw new ValidationError("direction must be directed | undirected");
|
|
5192
|
+
}
|
|
5193
|
+
const conditionSchema = parseConditionSchema(args.condition_schema);
|
|
5194
|
+
const isFlow = args.is_flow === true ? 1 : 0;
|
|
5195
|
+
let color = null;
|
|
5196
|
+
if (args.color !== void 0 && args.color !== null && args.color !== "") {
|
|
5197
|
+
if (typeof args.color !== "string" || !/^#[0-9a-f]{6}$/i.test(args.color)) {
|
|
5198
|
+
throw new ValidationError("color must be a 6-digit hex, e.g. #16a34a");
|
|
5199
|
+
}
|
|
5200
|
+
color = args.color;
|
|
5201
|
+
}
|
|
5202
|
+
const db = getDb();
|
|
5203
|
+
const definedBy = await resolveCallerEmail(ctx.userId);
|
|
5204
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5205
|
+
await seedDefaultEdgeTypes(ctx.nestId);
|
|
5206
|
+
let created = false;
|
|
5207
|
+
await db.transaction(async (tx) => {
|
|
5208
|
+
const existing = await tx.get(
|
|
5209
|
+
"SELECT id FROM edge_types WHERE nest_id = ? AND LOWER(name) = LOWER(?)",
|
|
5210
|
+
[ctx.nestId, name]
|
|
5211
|
+
);
|
|
5212
|
+
if (existing) {
|
|
5213
|
+
await tx.run(
|
|
5214
|
+
`UPDATE edge_types SET name = ?, description = ?, direction = ?, is_flow = ?,
|
|
5215
|
+
condition_schema = ?, color = ?, updated_at = ? WHERE id = ?`,
|
|
5216
|
+
[name, args.description.trim(), direction, isFlow, conditionSchema, color, now, existing.id]
|
|
5217
|
+
);
|
|
5218
|
+
} else {
|
|
5219
|
+
created = true;
|
|
5220
|
+
await tx.run(
|
|
5221
|
+
`INSERT INTO edge_types
|
|
5222
|
+
(id, nest_id, name, description, direction, is_flow, condition_schema, color, created_by, created_at, updated_at)
|
|
5223
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
5224
|
+
[uuid8(), ctx.nestId, name, args.description.trim(), direction, isFlow, conditionSchema, color, definedBy, now, now]
|
|
5225
|
+
);
|
|
5226
|
+
}
|
|
5227
|
+
});
|
|
5228
|
+
const row = await db.get(
|
|
5229
|
+
"SELECT * FROM edge_types WHERE nest_id = ? AND LOWER(name) = LOWER(?)",
|
|
5230
|
+
[ctx.nestId, name]
|
|
5231
|
+
);
|
|
5232
|
+
return { created, edge_type: edgeTypeToResponse(row) };
|
|
5233
|
+
}
|
|
5234
|
+
async function deleteEdgeType(nestId, id) {
|
|
5235
|
+
await getDb().transaction(async (tx) => {
|
|
5236
|
+
const row = await tx.get(
|
|
5237
|
+
"SELECT id FROM edge_types WHERE id = ? AND nest_id = ?",
|
|
5238
|
+
[id, nestId]
|
|
5239
|
+
);
|
|
5240
|
+
if (!row) throw new NotFoundError("Edge type not found");
|
|
5241
|
+
const inUse = await tx.get(
|
|
5242
|
+
"SELECT COUNT(*) as c FROM edges WHERE type_id = ?",
|
|
5243
|
+
[id]
|
|
5244
|
+
);
|
|
5245
|
+
if (inUse.c > 0) {
|
|
5246
|
+
throw new ValidationError(
|
|
5247
|
+
`Edge type is in use by ${inUse.c} edge${inUse.c === 1 ? "" : "s"} \u2014 delete those first.`
|
|
5248
|
+
);
|
|
5249
|
+
}
|
|
5250
|
+
await tx.run("DELETE FROM edge_types WHERE id = ?", [id]);
|
|
5251
|
+
});
|
|
5252
|
+
return { deleted: true, edge_type_id: id };
|
|
5253
|
+
}
|
|
5254
|
+
|
|
5255
|
+
// src/mcp/tools.ts
|
|
5256
|
+
var MAX_HOPS = 10;
|
|
5257
|
+
function normalizeHops(raw) {
|
|
5258
|
+
if (raw == null) return 2;
|
|
5259
|
+
const n = Number(raw);
|
|
5260
|
+
if (!Number.isFinite(n)) return 2;
|
|
5261
|
+
return Math.max(0, Math.min(MAX_HOPS, Math.floor(n)));
|
|
5262
|
+
}
|
|
5263
|
+
var TOOL_DEFINITIONS = [
|
|
5264
|
+
{
|
|
5265
|
+
name: "context_init",
|
|
5266
|
+
description: "Load the vault's CONTEXT.md which contains operating instructions and behavioral guidelines. Call this FIRST in every conversation.",
|
|
5267
|
+
inputSchema: { type: "object", properties: {} }
|
|
5268
|
+
},
|
|
5269
|
+
{
|
|
5270
|
+
name: "context_overview",
|
|
5271
|
+
description: "Get a complete map of the vault: total node count, types, tags, and a title+snippet for every node.",
|
|
5272
|
+
inputSchema: { type: "object", properties: {} }
|
|
5273
|
+
},
|
|
5274
|
+
{
|
|
5275
|
+
name: "context_search",
|
|
5276
|
+
description: "Full-text keyword search across all node content, titles, tags, and metadata.",
|
|
5277
|
+
inputSchema: {
|
|
5278
|
+
type: "object",
|
|
5279
|
+
properties: {
|
|
5280
|
+
query: { type: "string", description: "Search terms" }
|
|
5281
|
+
},
|
|
5282
|
+
required: ["query"]
|
|
5283
|
+
}
|
|
5284
|
+
},
|
|
5285
|
+
{
|
|
5286
|
+
name: "context_query",
|
|
5287
|
+
description: "Run a structured selector query. Supports: #tag, type:X, [[Title]], scope:X. Combine with +AND, |OR, -NOT.",
|
|
5288
|
+
inputSchema: {
|
|
5289
|
+
type: "object",
|
|
5290
|
+
properties: {
|
|
5291
|
+
query: { type: "string", description: "Selector query" },
|
|
5292
|
+
hops: {
|
|
5293
|
+
type: "number",
|
|
5294
|
+
description: "Graph traversal depth from the matched nodes (default: 2). Use 1 for just the matches + direct links, higher to pull in more of the neighborhood."
|
|
5295
|
+
}
|
|
5296
|
+
},
|
|
5297
|
+
required: ["query"]
|
|
5298
|
+
}
|
|
5299
|
+
},
|
|
5300
|
+
{
|
|
5301
|
+
name: "context_get",
|
|
5302
|
+
description: "Get the FULL content of a specific node by title or ID.",
|
|
5303
|
+
inputSchema: {
|
|
5304
|
+
type: "object",
|
|
5305
|
+
properties: {
|
|
5306
|
+
title: { type: "string", description: "Title of the node" },
|
|
5307
|
+
id: { type: "string", description: "ID of the node" }
|
|
5308
|
+
}
|
|
5309
|
+
}
|
|
5310
|
+
},
|
|
5311
|
+
{
|
|
5312
|
+
name: "context_list",
|
|
5313
|
+
description: "Browse vault contents with optional type, tag, or limit filters.",
|
|
5314
|
+
inputSchema: {
|
|
5315
|
+
type: "object",
|
|
5316
|
+
properties: {
|
|
5317
|
+
type: { type: "string", description: "Filter by node type" },
|
|
5318
|
+
tag: { type: "string", description: "Filter by tag" },
|
|
5319
|
+
limit: { type: "number", description: "Max nodes to return" }
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
},
|
|
5323
|
+
{
|
|
5324
|
+
name: "context_resolve",
|
|
5325
|
+
description: "Full context resolution \u2014 run a selector and return complete node content, respecting a token budget.",
|
|
5326
|
+
inputSchema: {
|
|
5327
|
+
type: "object",
|
|
5328
|
+
properties: {
|
|
5329
|
+
selector: {
|
|
5330
|
+
type: "string",
|
|
5331
|
+
description: "Selector query string"
|
|
5332
|
+
},
|
|
5333
|
+
max_tokens: {
|
|
5334
|
+
type: "number",
|
|
5335
|
+
description: "Approximate token budget (default: 8000)"
|
|
5336
|
+
},
|
|
5337
|
+
hops: {
|
|
5338
|
+
type: "number",
|
|
5339
|
+
description: "Graph traversal depth from the matched nodes (default: 2)"
|
|
5340
|
+
}
|
|
5341
|
+
},
|
|
5342
|
+
required: ["selector"]
|
|
5343
|
+
}
|
|
5344
|
+
},
|
|
5345
|
+
{
|
|
5346
|
+
name: "context_export",
|
|
5347
|
+
description: "Export the ENTIRE nest as one markdown bundle \u2014 every accessible document's FULL content. Use when you need the whole nest as context rather than a targeted query. Respects an optional token budget.",
|
|
5348
|
+
inputSchema: {
|
|
5349
|
+
type: "object",
|
|
5350
|
+
properties: {
|
|
5351
|
+
max_tokens: {
|
|
5352
|
+
type: "number",
|
|
5353
|
+
description: "Approximate token budget; documents are included until it's reached (omit for no cap)."
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
},
|
|
5358
|
+
{
|
|
4098
5359
|
name: "context_comments",
|
|
4099
5360
|
description: "Get the review comments/annotations on a document \u2014 anchored quotes, threaded replies, and open/resolved status \u2014 by title or ID. Use to pull human feedback into context.",
|
|
4100
5361
|
inputSchema: {
|
|
@@ -4263,6 +5524,26 @@ var TOOL_DEFINITIONS = [
|
|
|
4263
5524
|
required: ["email"]
|
|
4264
5525
|
}
|
|
4265
5526
|
},
|
|
5527
|
+
{
|
|
5528
|
+
name: "context_grant",
|
|
5529
|
+
description: "Share a SINGLE document or a FOLDER with a person (not the whole nest). The recipient can reach exactly that document (or everything under that folder) and nothing else. read = view, write = view+edit. Owner/admin only.",
|
|
5530
|
+
inputSchema: {
|
|
5531
|
+
type: "object",
|
|
5532
|
+
properties: {
|
|
5533
|
+
email: { type: "string", description: "Email of the person to share with" },
|
|
5534
|
+
target_type: {
|
|
5535
|
+
type: "string",
|
|
5536
|
+
description: "'document' to share one node, or 'folder' to share a path prefix"
|
|
5537
|
+
},
|
|
5538
|
+
target: {
|
|
5539
|
+
type: "string",
|
|
5540
|
+
description: "Node id (e.g. nodes/win-loss-log) for a document, or a folder prefix (e.g. nodes/gtm/deals) for a folder"
|
|
5541
|
+
},
|
|
5542
|
+
role: { type: "string", description: "read (default) or write" }
|
|
5543
|
+
},
|
|
5544
|
+
required: ["email", "target_type", "target"]
|
|
5545
|
+
}
|
|
5546
|
+
},
|
|
4266
5547
|
// ─── Unsynced Folder Tools ─────────────────────────────────────────
|
|
4267
5548
|
{
|
|
4268
5549
|
name: "context_unsynced_list",
|
|
@@ -4647,7 +5928,8 @@ ${list}`;
|
|
|
4647
5928
|
version: currentVersion,
|
|
4648
5929
|
requestedBy: ctx.userEmail,
|
|
4649
5930
|
note: args.note,
|
|
4650
|
-
priority: args.priority
|
|
5931
|
+
priority: args.priority,
|
|
5932
|
+
baseUrl: ctx.baseUrl
|
|
4651
5933
|
});
|
|
4652
5934
|
const resolved = await resolveStewardsForNode(
|
|
4653
5935
|
ctx.nestId,
|
|
@@ -4675,7 +5957,8 @@ ${resolved.map((r) => `- ${r.steward.userEmail} (${r.source})`).join("\n")}` : "
|
|
|
4675
5957
|
nodeId: node.id,
|
|
4676
5958
|
version: currentVersion,
|
|
4677
5959
|
approvedBy: ctx.userEmail,
|
|
4678
|
-
note: args.note
|
|
5960
|
+
note: args.note,
|
|
5961
|
+
baseUrl: ctx.baseUrl
|
|
4679
5962
|
});
|
|
4680
5963
|
return `Approved "${args.title}" v${currentVersion}. This version is now live for AI queries.${args.note ? `
|
|
4681
5964
|
Note: ${args.note}` : ""}`;
|
|
@@ -4696,7 +5979,8 @@ Note: ${args.note}` : ""}`;
|
|
|
4696
5979
|
nodeId: node.id,
|
|
4697
5980
|
version: currentVersion,
|
|
4698
5981
|
rejectedBy: ctx.userEmail,
|
|
4699
|
-
note: args.note
|
|
5982
|
+
note: args.note,
|
|
5983
|
+
baseUrl: ctx.baseUrl
|
|
4700
5984
|
});
|
|
4701
5985
|
return `Rejected "${args.title}" v${currentVersion}.
|
|
4702
5986
|
Reason: ${args.note}`;
|
|
@@ -4766,6 +6050,48 @@ ${list}`;
|
|
|
4766
6050
|
return `Failed to share nest: ${err.message}`;
|
|
4767
6051
|
}
|
|
4768
6052
|
}
|
|
6053
|
+
case "context_grant": {
|
|
6054
|
+
if (!await canManageStewards(ctx.nestId, ctx.userId)) {
|
|
6055
|
+
return "You don't have permission to share documents in this nest (owner/admin only).";
|
|
6056
|
+
}
|
|
6057
|
+
const targetType = String(args.target_type || "");
|
|
6058
|
+
const target = String(args.target || "").trim();
|
|
6059
|
+
const role = String(args.role || "read");
|
|
6060
|
+
if (!["document", "folder"].includes(targetType)) {
|
|
6061
|
+
return "target_type must be 'document' or 'folder'.";
|
|
6062
|
+
}
|
|
6063
|
+
if (!target) return "target is required (a node id or folder prefix).";
|
|
6064
|
+
if (!["read", "write"].includes(role)) return "role must be read or write.";
|
|
6065
|
+
try {
|
|
6066
|
+
const { createGrant: createGrant2 } = await import("./grants-service-MDLRPMZR.js");
|
|
6067
|
+
const db = (await import("./client-GI74NPIW.js")).getDb();
|
|
6068
|
+
const { normalizeEmail: normalizeEmail2 } = await import("./email-R7DFS6E5.js");
|
|
6069
|
+
const e = normalizeEmail2(String(args.email || ""));
|
|
6070
|
+
if (!e) return "email is required.";
|
|
6071
|
+
let row = await db.get("SELECT id FROM users WHERE LOWER(email) = ?", [e]);
|
|
6072
|
+
if (!row) {
|
|
6073
|
+
const { hashPassword: hashPassword2 } = await import("./keys-73STFJJB.js");
|
|
6074
|
+
const { v4: uuid12 } = await import("uuid");
|
|
6075
|
+
const id = uuid12();
|
|
6076
|
+
await db.run(
|
|
6077
|
+
"INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
|
|
6078
|
+
[id, e, null, await hashPassword2(uuid12())]
|
|
6079
|
+
);
|
|
6080
|
+
row = { id };
|
|
6081
|
+
}
|
|
6082
|
+
await createGrant2({
|
|
6083
|
+
nestId: ctx.nestId,
|
|
6084
|
+
targetType,
|
|
6085
|
+
target,
|
|
6086
|
+
userId: row.id,
|
|
6087
|
+
role,
|
|
6088
|
+
grantedBy: ctx.userEmail
|
|
6089
|
+
});
|
|
6090
|
+
return `Shared ${targetType} \`${target}\` with **${args.email}** as ${role}.`;
|
|
6091
|
+
} catch (err) {
|
|
6092
|
+
return `Failed to share: ${err.message}`;
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
4769
6095
|
case "context_unsynced_list": {
|
|
4770
6096
|
if (config.AUTH_MODE !== "open" && !await isLicenseAdminUserId(userId)) {
|
|
4771
6097
|
return "You don't have permission to list unsynced folders. Server admin only.";
|
|
@@ -4795,24 +6121,28 @@ ${list}`;
|
|
|
4795
6121
|
}
|
|
4796
6122
|
}
|
|
4797
6123
|
default:
|
|
6124
|
+
if (isWorkflowTool(toolName)) {
|
|
6125
|
+
return runWorkflowTool(toolName, args, ctx);
|
|
6126
|
+
}
|
|
4798
6127
|
return `Unknown tool: ${toolName}`;
|
|
4799
6128
|
}
|
|
4800
6129
|
}
|
|
4801
6130
|
|
|
4802
6131
|
// src/mcp/routes.ts
|
|
4803
6132
|
import { z } from "zod";
|
|
4804
|
-
var mcpRoutes = new
|
|
6133
|
+
var mcpRoutes = new Hono10();
|
|
4805
6134
|
async function getUserEmail2(userId) {
|
|
4806
6135
|
const db = getDb();
|
|
4807
6136
|
const user = await db.get("SELECT email FROM users WHERE id = ?", [userId]);
|
|
4808
6137
|
return user?.email || "anonymous@localhost";
|
|
4809
6138
|
}
|
|
4810
|
-
function createMcpServerForNest(nestId, userId, userEmail) {
|
|
6139
|
+
function createMcpServerForNest(nestId, userId, userEmail, baseUrl) {
|
|
4811
6140
|
const server = new McpServer(
|
|
4812
6141
|
{ name: `contextnest-${nestId}`, version: "1.0.0" },
|
|
4813
6142
|
{ capabilities: { tools: {} } }
|
|
4814
6143
|
);
|
|
4815
|
-
|
|
6144
|
+
const toolDefs = config.FEATURE_WORKFLOW_PLANE ? [...TOOL_DEFINITIONS, ...WORKFLOW_TOOL_DEFINITIONS] : TOOL_DEFINITIONS;
|
|
6145
|
+
for (const tool of toolDefs) {
|
|
4816
6146
|
const props = tool.inputSchema.properties || {};
|
|
4817
6147
|
const required = tool.inputSchema.required || [];
|
|
4818
6148
|
const shape = {};
|
|
@@ -4833,7 +6163,8 @@ function createMcpServerForNest(nestId, userId, userEmail) {
|
|
|
4833
6163
|
versionManager: engine.versions,
|
|
4834
6164
|
nestId,
|
|
4835
6165
|
userId,
|
|
4836
|
-
userEmail
|
|
6166
|
+
userEmail,
|
|
6167
|
+
baseUrl
|
|
4837
6168
|
});
|
|
4838
6169
|
return { content: [{ type: "text", text }] };
|
|
4839
6170
|
});
|
|
@@ -4844,7 +6175,12 @@ mcpRoutes.all("/", async (c) => {
|
|
|
4844
6175
|
const nestId = c.req.param("nestId");
|
|
4845
6176
|
const userId = c.get("userId");
|
|
4846
6177
|
const userEmail = await getUserEmail2(userId);
|
|
4847
|
-
const server = createMcpServerForNest(
|
|
6178
|
+
const server = createMcpServerForNest(
|
|
6179
|
+
nestId,
|
|
6180
|
+
userId,
|
|
6181
|
+
userEmail,
|
|
6182
|
+
requestBaseUrl(c.req.url)
|
|
6183
|
+
);
|
|
4848
6184
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
4849
6185
|
sessionIdGenerator: void 0,
|
|
4850
6186
|
enableJsonResponse: true
|
|
@@ -4859,8 +6195,256 @@ mcpRoutes.all("/", async (c) => {
|
|
|
4859
6195
|
}
|
|
4860
6196
|
});
|
|
4861
6197
|
|
|
6198
|
+
// src/workflow/run-routes.ts
|
|
6199
|
+
import { Hono as Hono11 } from "hono";
|
|
6200
|
+
var runTriggerRoutes = new Hono11();
|
|
6201
|
+
runTriggerRoutes.post("/:agentNode{.+}", requireWorkflowPlane, async (c) => {
|
|
6202
|
+
const nestId = c.req.param("nestId");
|
|
6203
|
+
const agentNode = assertSafeNodeId(c.req.param("agentNode"));
|
|
6204
|
+
const body = await c.req.json().catch(() => ({}));
|
|
6205
|
+
const { storage } = await engineCache.get(nestId);
|
|
6206
|
+
const triggeredBy = await resolveCallerEmail(c.get("userId"));
|
|
6207
|
+
const bundle = await triggerRun({
|
|
6208
|
+
nestId,
|
|
6209
|
+
agentNode,
|
|
6210
|
+
inputs: body.inputs,
|
|
6211
|
+
triggeredBy,
|
|
6212
|
+
storage,
|
|
6213
|
+
parentRunId: body.parent_run_id
|
|
6214
|
+
});
|
|
6215
|
+
return c.json(bundle, 201);
|
|
6216
|
+
});
|
|
6217
|
+
var runListRoutes = new Hono11();
|
|
6218
|
+
runListRoutes.get("/", requireWorkflowPlane, async (c) => {
|
|
6219
|
+
const nestId = c.req.param("nestId");
|
|
6220
|
+
const canSeeDetail = permissionLevel(c.get("nestPermission")) >= permissionLevel("write");
|
|
6221
|
+
const result = await listRuns(nestId, {
|
|
6222
|
+
agent: c.req.query("agent") || void 0,
|
|
6223
|
+
status: c.req.query("status") || void 0,
|
|
6224
|
+
limit: Number(c.req.query("limit")) || void 0,
|
|
6225
|
+
canSeeDetail
|
|
6226
|
+
});
|
|
6227
|
+
return c.json(result);
|
|
6228
|
+
});
|
|
6229
|
+
var runDetailRoutes = new Hono11();
|
|
6230
|
+
async function loadRunWithPermission(c, runId, required) {
|
|
6231
|
+
const run = await loadRun(runId);
|
|
6232
|
+
if (!run) throw new NotFoundError("Run not found");
|
|
6233
|
+
const nestScope = c.get("nestScope");
|
|
6234
|
+
if (nestScope && nestScope !== run.nest_id) {
|
|
6235
|
+
throw new ForbiddenError("API key not authorized for this nest");
|
|
6236
|
+
}
|
|
6237
|
+
if (required === "write") {
|
|
6238
|
+
if (isSuspended()) {
|
|
6239
|
+
throw new AppError(503, `Server suspended by PromptOwl: ${getSuspensionReason()}`);
|
|
6240
|
+
}
|
|
6241
|
+
if (!getCurrentLicense()?.valid) {
|
|
6242
|
+
throw new AppError(503, "A valid PromptOwl license is required to write runs.");
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
const perm = config.AUTH_MODE === "open" ? "owner" : await resolveNestPermission(run.nest_id, c.get("userId"));
|
|
6246
|
+
if (perm === "none") {
|
|
6247
|
+
throw new NotFoundError("Run not found");
|
|
6248
|
+
}
|
|
6249
|
+
if (permissionLevel(perm) < permissionLevel(required)) {
|
|
6250
|
+
throw new ForbiddenError(
|
|
6251
|
+
`This run belongs to a nest where you lack ${required} access.`
|
|
6252
|
+
);
|
|
6253
|
+
}
|
|
6254
|
+
c.set("runPermission", perm);
|
|
6255
|
+
return run;
|
|
6256
|
+
}
|
|
6257
|
+
runDetailRoutes.post("/:id/steps", requireWorkflowPlane, async (c) => {
|
|
6258
|
+
const run = await loadRunWithPermission(c, c.req.param("id"), "write");
|
|
6259
|
+
const body = await c.req.json();
|
|
6260
|
+
const { seq } = await appendRunStep(run.id, body);
|
|
6261
|
+
return c.json({ ok: true, seq }, 201);
|
|
6262
|
+
});
|
|
6263
|
+
runDetailRoutes.patch("/:id", requireWorkflowPlane, async (c) => {
|
|
6264
|
+
const run = await loadRunWithPermission(c, c.req.param("id"), "write");
|
|
6265
|
+
const body = await c.req.json();
|
|
6266
|
+
const { status } = await closeRun(run.id, body);
|
|
6267
|
+
return c.json({ ok: true, status });
|
|
6268
|
+
});
|
|
6269
|
+
runDetailRoutes.get("/:id", requireWorkflowPlane, async (c) => {
|
|
6270
|
+
const run = await loadRunWithPermission(c, c.req.param("id"), "read");
|
|
6271
|
+
const canSeeDetail = permissionLevel(c.get("runPermission")) >= permissionLevel("write");
|
|
6272
|
+
const result = await getRunTrace(run, { canSeeDetail });
|
|
6273
|
+
return c.json(result);
|
|
6274
|
+
});
|
|
6275
|
+
|
|
6276
|
+
// src/definitions/routes.ts
|
|
6277
|
+
import { Hono as Hono12 } from "hono";
|
|
6278
|
+
import { v4 as uuid9 } from "uuid";
|
|
6279
|
+
var definitionRoutes = new Hono12();
|
|
6280
|
+
definitionRoutes.get("/", async (c) => {
|
|
6281
|
+
const nestId = c.req.param("nestId");
|
|
6282
|
+
const db = getDb();
|
|
6283
|
+
const term = c.req.query("term")?.trim();
|
|
6284
|
+
if (term) {
|
|
6285
|
+
const row = await db.get(
|
|
6286
|
+
"SELECT * FROM definitions WHERE nest_id = ? AND LOWER(term) = LOWER(?)",
|
|
6287
|
+
[nestId, term]
|
|
6288
|
+
);
|
|
6289
|
+
if (!row) throw new NotFoundError(`No definition for "${term}"`);
|
|
6290
|
+
return c.json({ definition: row });
|
|
6291
|
+
}
|
|
6292
|
+
const rows = await db.all(
|
|
6293
|
+
"SELECT * FROM definitions WHERE nest_id = ? ORDER BY LOWER(term)",
|
|
6294
|
+
[nestId]
|
|
6295
|
+
);
|
|
6296
|
+
return c.json({ count: rows.length, definitions: rows });
|
|
6297
|
+
});
|
|
6298
|
+
definitionRoutes.post("/", async (c) => {
|
|
6299
|
+
const nestId = c.req.param("nestId");
|
|
6300
|
+
const body = await c.req.json();
|
|
6301
|
+
if (body.term !== void 0 && typeof body.term !== "string")
|
|
6302
|
+
throw new ValidationError("term must be a string");
|
|
6303
|
+
if (body.definition !== void 0 && typeof body.definition !== "string")
|
|
6304
|
+
throw new ValidationError("definition must be a string");
|
|
6305
|
+
if (body.linked_tag !== void 0 && body.linked_tag !== null && typeof body.linked_tag !== "string")
|
|
6306
|
+
throw new ValidationError("linked_tag must be a string or null");
|
|
6307
|
+
if (body.editing_id !== void 0 && typeof body.editing_id !== "string")
|
|
6308
|
+
throw new ValidationError("editing_id must be a string");
|
|
6309
|
+
const term = body.term?.trim();
|
|
6310
|
+
const definition = body.definition?.trim();
|
|
6311
|
+
const editingId = body.editing_id?.trim() || void 0;
|
|
6312
|
+
if (!term || !definition) {
|
|
6313
|
+
throw new ValidationError("term and definition are required");
|
|
6314
|
+
}
|
|
6315
|
+
if (term.length > 200) {
|
|
6316
|
+
throw new ValidationError("term must be 200 characters or fewer");
|
|
6317
|
+
}
|
|
6318
|
+
const linkedTagProvided = "linked_tag" in body;
|
|
6319
|
+
const linkedTag = body.linked_tag?.trim() ? `#${body.linked_tag.trim().replace(/^#+/, "").toLowerCase()}` : null;
|
|
6320
|
+
const db = getDb();
|
|
6321
|
+
const definedBy = await resolveCallerEmail(c.get("userId"));
|
|
6322
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6323
|
+
const applyUpdate = async (tx, id) => {
|
|
6324
|
+
if (linkedTagProvided) {
|
|
6325
|
+
await tx.run(
|
|
6326
|
+
"UPDATE definitions SET term = ?, definition = ?, linked_tag = ?, defined_by = ?, updated_at = ? WHERE id = ?",
|
|
6327
|
+
[term, definition, linkedTag, definedBy, now, id]
|
|
6328
|
+
);
|
|
6329
|
+
} else {
|
|
6330
|
+
await tx.run(
|
|
6331
|
+
"UPDATE definitions SET term = ?, definition = ?, defined_by = ?, updated_at = ? WHERE id = ?",
|
|
6332
|
+
[term, definition, definedBy, now, id]
|
|
6333
|
+
);
|
|
6334
|
+
}
|
|
6335
|
+
};
|
|
6336
|
+
const { row, created } = await db.transaction(async (tx) => {
|
|
6337
|
+
const existing = await tx.get(
|
|
6338
|
+
"SELECT id FROM definitions WHERE nest_id = ? AND LOWER(term) = LOWER(?)",
|
|
6339
|
+
[nestId, term]
|
|
6340
|
+
);
|
|
6341
|
+
let targetId;
|
|
6342
|
+
let wasCreated = false;
|
|
6343
|
+
if (editingId) {
|
|
6344
|
+
const editing = await tx.get(
|
|
6345
|
+
"SELECT id FROM definitions WHERE id = ? AND nest_id = ?",
|
|
6346
|
+
[editingId, nestId]
|
|
6347
|
+
);
|
|
6348
|
+
if (!editing) throw new NotFoundError("Definition not found");
|
|
6349
|
+
if (existing && existing.id !== editingId) {
|
|
6350
|
+
throw new ConflictError(`A definition for "${term}" already exists`);
|
|
6351
|
+
}
|
|
6352
|
+
await applyUpdate(tx, editingId);
|
|
6353
|
+
targetId = editingId;
|
|
6354
|
+
} else if (existing) {
|
|
6355
|
+
await applyUpdate(tx, existing.id);
|
|
6356
|
+
targetId = existing.id;
|
|
6357
|
+
} else {
|
|
6358
|
+
targetId = uuid9();
|
|
6359
|
+
wasCreated = true;
|
|
6360
|
+
await tx.run(
|
|
6361
|
+
`INSERT INTO definitions (id, nest_id, term, definition, linked_tag, defined_by, created_at, updated_at)
|
|
6362
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
6363
|
+
[targetId, nestId, term, definition, linkedTag, definedBy, now, now]
|
|
6364
|
+
);
|
|
6365
|
+
}
|
|
6366
|
+
const fresh = await tx.get("SELECT * FROM definitions WHERE id = ?", [
|
|
6367
|
+
targetId
|
|
6368
|
+
]);
|
|
6369
|
+
return { row: fresh, created: wasCreated };
|
|
6370
|
+
});
|
|
6371
|
+
return c.json({ definition: row }, created ? 201 : 200);
|
|
6372
|
+
});
|
|
6373
|
+
definitionRoutes.delete("/:id", async (c) => {
|
|
6374
|
+
const nestId = c.req.param("nestId");
|
|
6375
|
+
const id = c.req.param("id");
|
|
6376
|
+
const db = getDb();
|
|
6377
|
+
const row = await db.get(
|
|
6378
|
+
"SELECT id FROM definitions WHERE id = ? AND nest_id = ?",
|
|
6379
|
+
[id, nestId]
|
|
6380
|
+
);
|
|
6381
|
+
if (!row) throw new NotFoundError("Definition not found");
|
|
6382
|
+
await db.run("DELETE FROM definitions WHERE id = ?", [id]);
|
|
6383
|
+
return c.json({ deleted: true });
|
|
6384
|
+
});
|
|
6385
|
+
|
|
6386
|
+
// src/nests/asset-routes.ts
|
|
6387
|
+
import { Hono as Hono13 } from "hono";
|
|
6388
|
+
import { v4 as uuid10 } from "uuid";
|
|
6389
|
+
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
6390
|
+
import { join as join3 } from "path";
|
|
6391
|
+
var assetRoutes = new Hono13();
|
|
6392
|
+
var CONTENT_TYPES = {
|
|
6393
|
+
png: "image/png",
|
|
6394
|
+
jpg: "image/jpeg",
|
|
6395
|
+
jpeg: "image/jpeg",
|
|
6396
|
+
gif: "image/gif",
|
|
6397
|
+
webp: "image/webp"
|
|
6398
|
+
};
|
|
6399
|
+
var SAFE_NAME = /^[0-9a-f-]{36}\.(png|jpe?g|gif|webp)$/;
|
|
6400
|
+
assetRoutes.post("/", async (c) => {
|
|
6401
|
+
const nestId = c.req.param("nestId");
|
|
6402
|
+
const body = await c.req.parseBody();
|
|
6403
|
+
const file = body["file"];
|
|
6404
|
+
if (!(file instanceof File)) {
|
|
6405
|
+
throw new ValidationError('multipart field "file" (an image) is required');
|
|
6406
|
+
}
|
|
6407
|
+
const ext = (file.name.split(".").pop() || "").toLowerCase();
|
|
6408
|
+
if (!CONTENT_TYPES[ext]) {
|
|
6409
|
+
throw new ValidationError(
|
|
6410
|
+
`unsupported image type ".${ext}" \u2014 allowed: ${Object.keys(CONTENT_TYPES).join(", ")}`
|
|
6411
|
+
);
|
|
6412
|
+
}
|
|
6413
|
+
const name = `${uuid10()}.${ext === "jpeg" ? "jpg" : ext}`;
|
|
6414
|
+
const dir = join3(resolveNestPath(nestId), "assets");
|
|
6415
|
+
await mkdir(dir, { recursive: true });
|
|
6416
|
+
await writeFile(join3(dir, name), Buffer.from(await file.arrayBuffer()));
|
|
6417
|
+
return c.json(
|
|
6418
|
+
{
|
|
6419
|
+
file: name,
|
|
6420
|
+
url: `/nests/${nestId}/assets/${name}`,
|
|
6421
|
+
markdown: `![${file.name.replace(/\.[^.]+$/, "")}](/nests/${nestId}/assets/${name})`
|
|
6422
|
+
},
|
|
6423
|
+
201
|
|
6424
|
+
);
|
|
6425
|
+
});
|
|
6426
|
+
assetRoutes.get("/:file", async (c) => {
|
|
6427
|
+
const nestId = c.req.param("nestId");
|
|
6428
|
+
const name = c.req.param("file");
|
|
6429
|
+
if (!SAFE_NAME.test(name)) {
|
|
6430
|
+
throw new ValidationError("invalid asset name");
|
|
6431
|
+
}
|
|
6432
|
+
let bytes;
|
|
6433
|
+
try {
|
|
6434
|
+
bytes = await readFile2(join3(resolveNestPath(nestId), "assets", name));
|
|
6435
|
+
} catch {
|
|
6436
|
+
throw new NotFoundError("Asset not found");
|
|
6437
|
+
}
|
|
6438
|
+
const ext = name.split(".").pop();
|
|
6439
|
+
return c.body(new Uint8Array(bytes), 200, {
|
|
6440
|
+
"Content-Type": CONTENT_TYPES[ext],
|
|
6441
|
+
// uuid names never change content — cache hard.
|
|
6442
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
6443
|
+
});
|
|
6444
|
+
});
|
|
6445
|
+
|
|
4862
6446
|
// src/mcp/server-routes.ts
|
|
4863
|
-
import { Hono as
|
|
6447
|
+
import { Hono as Hono14 } from "hono";
|
|
4864
6448
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4865
6449
|
import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
4866
6450
|
import { z as z2 } from "zod";
|
|
@@ -5065,7 +6649,8 @@ async function handleServerToolCall(toolName, args, userId, userEmail, nestScope
|
|
|
5065
6649
|
versionManager: engine.versions,
|
|
5066
6650
|
nestId: nest.id,
|
|
5067
6651
|
userId,
|
|
5068
|
-
userEmail
|
|
6652
|
+
userEmail,
|
|
6653
|
+
baseUrl
|
|
5069
6654
|
});
|
|
5070
6655
|
}
|
|
5071
6656
|
function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
@@ -5100,15 +6685,7 @@ function createServerMcp(userId, userEmail, nestScope, baseUrl) {
|
|
|
5100
6685
|
}
|
|
5101
6686
|
return server;
|
|
5102
6687
|
}
|
|
5103
|
-
|
|
5104
|
-
if (config.PUBLIC_BASE_URL) return config.PUBLIC_BASE_URL;
|
|
5105
|
-
try {
|
|
5106
|
-
return new URL(reqUrl).origin;
|
|
5107
|
-
} catch {
|
|
5108
|
-
return "";
|
|
5109
|
-
}
|
|
5110
|
-
}
|
|
5111
|
-
var serverMcpRoutes = new Hono8();
|
|
6688
|
+
var serverMcpRoutes = new Hono14();
|
|
5112
6689
|
serverMcpRoutes.all("/", async (c) => {
|
|
5113
6690
|
const userId = c.get("userId");
|
|
5114
6691
|
const userEmail = await getUserEmail3(userId);
|
|
@@ -5130,7 +6707,7 @@ serverMcpRoutes.all("/", async (c) => {
|
|
|
5130
6707
|
await server.server.close();
|
|
5131
6708
|
}
|
|
5132
6709
|
});
|
|
5133
|
-
var nestIndexRoutes = new
|
|
6710
|
+
var nestIndexRoutes = new Hono14();
|
|
5134
6711
|
nestIndexRoutes.get("/", async (c) => {
|
|
5135
6712
|
const userId = c.get("userId");
|
|
5136
6713
|
const nests = await buildNestIndex(userId, c.get("nestScope"));
|
|
@@ -5252,17 +6829,17 @@ npx -y mcp-remote ${B}/mcp --header "Authorization: Bearer <api-key>"
|
|
|
5252
6829
|
through steward review before they become AI-visible.
|
|
5253
6830
|
`;
|
|
5254
6831
|
}
|
|
5255
|
-
var llmsTxtRoutes = new
|
|
6832
|
+
var llmsTxtRoutes = new Hono14();
|
|
5256
6833
|
llmsTxtRoutes.get("/", (c) => {
|
|
5257
6834
|
const md = renderLlmsTxt(requestBaseUrl(c.req.url));
|
|
5258
6835
|
return c.text(md, 200, { "Content-Type": "text/markdown; charset=utf-8" });
|
|
5259
6836
|
});
|
|
5260
6837
|
|
|
5261
6838
|
// src/governance/routes.ts
|
|
5262
|
-
import { Hono as
|
|
6839
|
+
import { Hono as Hono15 } from "hono";
|
|
5263
6840
|
|
|
5264
6841
|
// src/governance/comment-service.ts
|
|
5265
|
-
import { v4 as
|
|
6842
|
+
import { v4 as uuid11 } from "uuid";
|
|
5266
6843
|
async function createComment(params) {
|
|
5267
6844
|
const db = getDb();
|
|
5268
6845
|
const body = (params.body ?? "").trim();
|
|
@@ -5278,7 +6855,7 @@ async function createComment(params) {
|
|
|
5278
6855
|
throw new Error("Parent comment not found on this node");
|
|
5279
6856
|
}
|
|
5280
6857
|
}
|
|
5281
|
-
const id =
|
|
6858
|
+
const id = uuid11();
|
|
5282
6859
|
await db.run(
|
|
5283
6860
|
`INSERT INTO comments
|
|
5284
6861
|
(id, nest_id, node_id, version, anchor_start, anchor_end, anchor_text,
|
|
@@ -5478,7 +7055,7 @@ function rowToComment(row) {
|
|
|
5478
7055
|
|
|
5479
7056
|
// src/governance/stewards-parser.ts
|
|
5480
7057
|
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
5481
|
-
import { join as
|
|
7058
|
+
import { join as join4 } from "path";
|
|
5482
7059
|
function parseStewardsYaml(content) {
|
|
5483
7060
|
const result = { version: 1 };
|
|
5484
7061
|
const lines = content.split("\n");
|
|
@@ -5544,9 +7121,9 @@ function parseEntry(str) {
|
|
|
5544
7121
|
function loadStewardsConfig(nestId) {
|
|
5545
7122
|
const nestPath = resolveNestPath(nestId);
|
|
5546
7123
|
const candidates = [
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
7124
|
+
join4(nestPath, "stewards.yaml"),
|
|
7125
|
+
join4(nestPath, "stewards.yml"),
|
|
7126
|
+
join4(nestPath, ".context", "stewards.yaml")
|
|
5550
7127
|
];
|
|
5551
7128
|
for (const candidatePath of candidates) {
|
|
5552
7129
|
if (existsSync(candidatePath)) {
|
|
@@ -5563,7 +7140,7 @@ function parseLimit(raw, def = 100, max = 1e3) {
|
|
|
5563
7140
|
if (Number.isNaN(n) || n < 1) return def;
|
|
5564
7141
|
return Math.min(n, max);
|
|
5565
7142
|
}
|
|
5566
|
-
var governanceRoutes = new
|
|
7143
|
+
var governanceRoutes = new Hono15();
|
|
5567
7144
|
governanceRoutes.get("/stewards", async (c) => {
|
|
5568
7145
|
const nestId = c.req.param("nestId");
|
|
5569
7146
|
const scope = c.req.query("scope");
|
|
@@ -5701,7 +7278,7 @@ governanceRoutes.get("/activity", async (c) => {
|
|
|
5701
7278
|
const activity = await getActivity({ nestId, limit });
|
|
5702
7279
|
return c.json({ activity });
|
|
5703
7280
|
});
|
|
5704
|
-
var governanceNodeRoutes = new
|
|
7281
|
+
var governanceNodeRoutes = new Hono15();
|
|
5705
7282
|
governanceNodeRoutes.get("/:nodeId{.+}/stewards", async (c) => {
|
|
5706
7283
|
const nestId = c.req.param("nestId");
|
|
5707
7284
|
const nodeId = c.req.param("nodeId");
|
|
@@ -5826,7 +7403,8 @@ governanceNodeRoutes.post("/:nodeId{.+}/submit-review", async (c) => {
|
|
|
5826
7403
|
version: currentVersion,
|
|
5827
7404
|
requestedBy: userEmail,
|
|
5828
7405
|
note: body.note,
|
|
5829
|
-
priority: body.priority
|
|
7406
|
+
priority: body.priority,
|
|
7407
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
5830
7408
|
});
|
|
5831
7409
|
} catch (err) {
|
|
5832
7410
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -5861,7 +7439,8 @@ governanceNodeRoutes.post("/:nodeId{.+}/approve", async (c) => {
|
|
|
5861
7439
|
version: await getCurrentVersion(nestId, nodeId),
|
|
5862
7440
|
approvedBy: userEmail,
|
|
5863
7441
|
note: body.note,
|
|
5864
|
-
override: body.override && isAdmin
|
|
7442
|
+
override: body.override && isAdmin,
|
|
7443
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
5865
7444
|
});
|
|
5866
7445
|
return c.json({ review: request });
|
|
5867
7446
|
} catch (err) {
|
|
@@ -5882,7 +7461,8 @@ governanceNodeRoutes.post("/:nodeId{.+}/reject", async (c) => {
|
|
|
5882
7461
|
nodeId,
|
|
5883
7462
|
version: await getCurrentVersion(nestId, nodeId),
|
|
5884
7463
|
rejectedBy: userEmail,
|
|
5885
|
-
note: body.note
|
|
7464
|
+
note: body.note,
|
|
7465
|
+
baseUrl: requestBaseUrl(c.req.url)
|
|
5886
7466
|
});
|
|
5887
7467
|
return c.json({ review: request });
|
|
5888
7468
|
} catch (err) {
|
|
@@ -6012,30 +7592,6 @@ async function getUserEmail4(c) {
|
|
|
6012
7592
|
return user?.email || "anonymous@localhost";
|
|
6013
7593
|
}
|
|
6014
7594
|
|
|
6015
|
-
// src/shared/node-id.ts
|
|
6016
|
-
function assertSafeNodeId(rawId) {
|
|
6017
|
-
let id = rawId;
|
|
6018
|
-
try {
|
|
6019
|
-
id = decodeURIComponent(rawId);
|
|
6020
|
-
} catch {
|
|
6021
|
-
}
|
|
6022
|
-
if (!id || id.length > 512) {
|
|
6023
|
-
throw new ValidationError("invalid node id");
|
|
6024
|
-
}
|
|
6025
|
-
if (id.includes("\0") || id.includes("\\")) {
|
|
6026
|
-
throw new ValidationError("invalid node id");
|
|
6027
|
-
}
|
|
6028
|
-
if (id.startsWith("/")) {
|
|
6029
|
-
throw new ValidationError("invalid node id (absolute path)");
|
|
6030
|
-
}
|
|
6031
|
-
for (const seg of id.split("/")) {
|
|
6032
|
-
if (seg === "" || seg === "." || seg === "..") {
|
|
6033
|
-
throw new ValidationError("invalid node id (path traversal)");
|
|
6034
|
-
}
|
|
6035
|
-
}
|
|
6036
|
-
return id;
|
|
6037
|
-
}
|
|
6038
|
-
|
|
6039
7595
|
// src/auth/anonymous.ts
|
|
6040
7596
|
import bcrypt from "bcryptjs";
|
|
6041
7597
|
async function ensureAnonymousUser() {
|
|
@@ -6057,12 +7613,12 @@ async function ensureAnonymousUser() {
|
|
|
6057
7613
|
// src/app.ts
|
|
6058
7614
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
6059
7615
|
import { fileURLToPath } from "url";
|
|
6060
|
-
import { dirname, join as
|
|
7616
|
+
import { dirname, join as join5, relative as relative2 } from "path";
|
|
6061
7617
|
import { existsSync as existsSync2 } from "fs";
|
|
6062
7618
|
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
6063
7619
|
var UI_DIR_CANDIDATES = [
|
|
6064
|
-
|
|
6065
|
-
|
|
7620
|
+
join5(HERE, "web3"),
|
|
7621
|
+
join5(process.cwd(), "dist", "web3")
|
|
6066
7622
|
];
|
|
6067
7623
|
var UI_DIR_ABS = UI_DIR_CANDIDATES.find((p) => existsSync2(p)) || UI_DIR_CANDIDATES[0];
|
|
6068
7624
|
var UI_DIR_REL = relative2(process.cwd(), UI_DIR_ABS) || ".";
|
|
@@ -6099,7 +7655,7 @@ var flexAuthMiddleware = createMiddleware2(async (c, next) => {
|
|
|
6099
7655
|
return c.json({ error: "Missing or invalid credentials" }, 401);
|
|
6100
7656
|
});
|
|
6101
7657
|
function createApp() {
|
|
6102
|
-
const app = new
|
|
7658
|
+
const app = new Hono16({ router: new LinearRouter() });
|
|
6103
7659
|
const corsOrigins = config.CORS_ORIGINS;
|
|
6104
7660
|
app.use(
|
|
6105
7661
|
"*",
|
|
@@ -6145,6 +7701,14 @@ function createApp() {
|
|
|
6145
7701
|
auth_mode: config.AUTH_MODE,
|
|
6146
7702
|
logo_url: config.LOGO_URL,
|
|
6147
7703
|
promptowl_sign_in_gate: config.PROMPTOWL_SIGN_IN_GATE,
|
|
7704
|
+
// Public feature flags the SPA needs before auth to decide what to
|
|
7705
|
+
// render. Mirrors the server-side route gate so UI and API agree:
|
|
7706
|
+
// flag off → the plane's routes 404 AND its UI stays hidden.
|
|
7707
|
+
workflow_plane_enabled: config.FEATURE_WORKFLOW_PLANE,
|
|
7708
|
+
// Sub-agent runs gate its own recursion surface separately (env-only,
|
|
7709
|
+
// must clear a security review — NOT a UI toggle). Exposed read-only so
|
|
7710
|
+
// the Runs page shows the "spawn sub-agent" option only when it's live.
|
|
7711
|
+
subagent_runs_enabled: config.FEATURE_SUBAGENT_RUNS,
|
|
6148
7712
|
...isSuspended() && { suspended_reason: getSuspensionReason() }
|
|
6149
7713
|
})
|
|
6150
7714
|
);
|
|
@@ -6236,7 +7800,12 @@ function createApp() {
|
|
|
6236
7800
|
telemetry_enabled: config.TELEMETRY_ENABLED,
|
|
6237
7801
|
public_base_url: config.PUBLIC_BASE_URL,
|
|
6238
7802
|
max_body_bytes: config.MAX_BODY_BYTES,
|
|
6239
|
-
|
|
7803
|
+
workflow_plane_enabled: config.FEATURE_WORKFLOW_PLANE,
|
|
7804
|
+
subagent_runs_enabled: config.FEATURE_SUBAGENT_RUNS,
|
|
7805
|
+
slack_webhook_url: config.SLACK_WEBHOOK_URL ?? "",
|
|
7806
|
+
smtp_url: config.SMTP_URL ?? "",
|
|
7807
|
+
notify_email_from: config.NOTIFY_EMAIL_FROM ?? "",
|
|
7808
|
+
notify_email_to: config.NOTIFY_EMAIL_TO ?? ""
|
|
6240
7809
|
});
|
|
6241
7810
|
app.get("/admin/settings", async (c) => {
|
|
6242
7811
|
if (!await adminSettingsAllowed(c))
|
|
@@ -6271,12 +7840,49 @@ function createApp() {
|
|
|
6271
7840
|
if ("telemetry_enabled" in body) {
|
|
6272
7841
|
pending.push({ name: "TELEMETRY_ENABLED", value: body.telemetry_enabled ? "true" : "false" });
|
|
6273
7842
|
}
|
|
7843
|
+
if ("workflow_plane_enabled" in body) {
|
|
7844
|
+
pending.push({
|
|
7845
|
+
name: "FEATURE_WORKFLOW_PLANE",
|
|
7846
|
+
value: body.workflow_plane_enabled ? "true" : null
|
|
7847
|
+
});
|
|
7848
|
+
}
|
|
7849
|
+
if ("subagent_runs_enabled" in body) {
|
|
7850
|
+
const planeOn = "workflow_plane_enabled" in body ? !!body.workflow_plane_enabled : config.FEATURE_WORKFLOW_PLANE;
|
|
7851
|
+
if (body.subagent_runs_enabled && !planeOn) {
|
|
7852
|
+
errors.push(
|
|
7853
|
+
"subagent_runs_enabled requires workflow_plane_enabled to be on"
|
|
7854
|
+
);
|
|
7855
|
+
} else {
|
|
7856
|
+
pending.push({
|
|
7857
|
+
name: "FEATURE_SUBAGENT_RUNS",
|
|
7858
|
+
value: body.subagent_runs_enabled ? "true" : null
|
|
7859
|
+
});
|
|
7860
|
+
}
|
|
7861
|
+
}
|
|
6274
7862
|
if ("slack_webhook_url" in body) {
|
|
6275
7863
|
const v = String(body.slack_webhook_url ?? "").trim();
|
|
6276
7864
|
if (v && !/^https:\/\//i.test(v))
|
|
6277
7865
|
errors.push("slack_webhook_url must be an https:// URL (or empty to disable)");
|
|
6278
7866
|
else pending.push({ name: "SLACK_WEBHOOK_URL", value: v || null });
|
|
6279
7867
|
}
|
|
7868
|
+
if ("smtp_url" in body) {
|
|
7869
|
+
const v = String(body.smtp_url ?? "").trim();
|
|
7870
|
+
if (v && !/^smtps?:\/\//i.test(v))
|
|
7871
|
+
errors.push("smtp_url must be an smtp:// or smtps:// URL (or empty to disable)");
|
|
7872
|
+
else pending.push({ name: "SMTP_URL", value: v || null });
|
|
7873
|
+
}
|
|
7874
|
+
if ("notify_email_from" in body) {
|
|
7875
|
+
const v = String(body.notify_email_from ?? "").trim();
|
|
7876
|
+
if (v && !isEmailish(v))
|
|
7877
|
+
errors.push("notify_email_from must be a plain email address");
|
|
7878
|
+
else pending.push({ name: "NOTIFY_EMAIL_FROM", value: v || null });
|
|
7879
|
+
}
|
|
7880
|
+
if ("notify_email_to" in body) {
|
|
7881
|
+
const v = String(body.notify_email_to ?? "").trim();
|
|
7882
|
+
if (v && !isEmailListish(v))
|
|
7883
|
+
errors.push("notify_email_to must be a comma-separated list of email addresses");
|
|
7884
|
+
else pending.push({ name: "NOTIFY_EMAIL_TO", value: v || null });
|
|
7885
|
+
}
|
|
6280
7886
|
if ("max_body_bytes" in body) {
|
|
6281
7887
|
const n = Number(body.max_body_bytes);
|
|
6282
7888
|
if (!Number.isFinite(n) || n < 1024 * 1024 || n > 500 * 1024 * 1024)
|
|
@@ -6351,12 +7957,14 @@ function createApp() {
|
|
|
6351
7957
|
users: usersRow.c
|
|
6352
7958
|
});
|
|
6353
7959
|
});
|
|
7960
|
+
app.use("/runs/*", flexAuthMiddleware);
|
|
7961
|
+
app.route("/runs", runDetailRoutes);
|
|
6354
7962
|
app.route("/llms.txt", llmsTxtRoutes);
|
|
6355
7963
|
app.use("/index", flexAuthMiddleware);
|
|
6356
7964
|
app.route("/index", nestIndexRoutes);
|
|
6357
7965
|
app.use("/mcp", flexAuthMiddleware);
|
|
6358
7966
|
app.route("/mcp", serverMcpRoutes);
|
|
6359
|
-
const nestsApp = new
|
|
7967
|
+
const nestsApp = new Hono16();
|
|
6360
7968
|
nestsApp.use("*", flexAuthMiddleware);
|
|
6361
7969
|
nestsApp.use("*", async (c, next) => {
|
|
6362
7970
|
const localPath = c.req.path.replace(/^\/nests\//, "");
|
|
@@ -6404,8 +8012,27 @@ function createApp() {
|
|
|
6404
8012
|
return next();
|
|
6405
8013
|
}
|
|
6406
8014
|
const permission = await resolveNestPermission(nestId, userId);
|
|
8015
|
+
const isReadQuery = parts.length === 2 && ["context", "query", "search", "export"].includes(parts[1]);
|
|
8016
|
+
let grantRole = null;
|
|
8017
|
+
let grantNestVisible = false;
|
|
6407
8018
|
if (permission === "none") {
|
|
6408
|
-
|
|
8019
|
+
if (parts[1] === "nodes" && parts.length >= 3) {
|
|
8020
|
+
let targetTail = parts.slice(2).join("/");
|
|
8021
|
+
try {
|
|
8022
|
+
targetTail = decodeURIComponent(targetTail);
|
|
8023
|
+
} catch {
|
|
8024
|
+
}
|
|
8025
|
+
grantRole = await resolveNodeGrant(nestId, userId, targetTail);
|
|
8026
|
+
} else if (parts.length === 1 && c.req.method === "GET") {
|
|
8027
|
+
grantNestVisible = await hasAnyGrant(nestId, userId);
|
|
8028
|
+
} else if (parts[1] === "nodes" && parts.length === 2 && c.req.method === "GET") {
|
|
8029
|
+
grantNestVisible = await hasAnyGrant(nestId, userId);
|
|
8030
|
+
} else if (isReadQuery) {
|
|
8031
|
+
grantNestVisible = await hasAnyGrant(nestId, userId);
|
|
8032
|
+
}
|
|
8033
|
+
if (!grantRole && !grantNestVisible) {
|
|
8034
|
+
return c.json({ error: "Nest not found" }, 404);
|
|
8035
|
+
}
|
|
6409
8036
|
}
|
|
6410
8037
|
let required = "read";
|
|
6411
8038
|
const path = c.req.path;
|
|
@@ -6426,7 +8053,7 @@ function createApp() {
|
|
|
6426
8053
|
required = "admin";
|
|
6427
8054
|
} else if (resource === "collaborators") {
|
|
6428
8055
|
required = c.req.method === "GET" || c.req.method === "POST" ? "write" : "admin";
|
|
6429
|
-
} else if (c.req.method !== "GET" && !isStewardActionPath && !isCommentAction && !isAnnotationAction) {
|
|
8056
|
+
} else if (c.req.method !== "GET" && !isStewardActionPath && !isCommentAction && !isAnnotationAction && !isReadQuery) {
|
|
6430
8057
|
required = "write";
|
|
6431
8058
|
}
|
|
6432
8059
|
const isNodeRevert = c.req.method === "POST" && parts.length >= 4 && parts[parts.length - 1] === "revert";
|
|
@@ -6452,7 +8079,9 @@ function createApp() {
|
|
|
6452
8079
|
);
|
|
6453
8080
|
}
|
|
6454
8081
|
}
|
|
6455
|
-
|
|
8082
|
+
const grantLevel = grantRole ? permissionLevel(grantRole) : grantNestVisible ? permissionLevel("read") : 0;
|
|
8083
|
+
const effectiveLevel = Math.max(permissionLevel(permission), grantLevel);
|
|
8084
|
+
if (!stewardEditorBypass && effectiveLevel < permissionLevel(required)) {
|
|
6456
8085
|
return c.json(
|
|
6457
8086
|
{
|
|
6458
8087
|
error: `You don't have access to perform this action on this nest. Required permission: '${required}', your permission: '${permission}'. Ask the nest owner or a server admin to grant you ${required} access.`,
|
|
@@ -6462,7 +8091,10 @@ function createApp() {
|
|
|
6462
8091
|
403
|
|
6463
8092
|
);
|
|
6464
8093
|
}
|
|
6465
|
-
c.set(
|
|
8094
|
+
c.set(
|
|
8095
|
+
"nestPermission",
|
|
8096
|
+
permission === "none" && grantRole ? grantRole : permission
|
|
8097
|
+
);
|
|
6466
8098
|
return next();
|
|
6467
8099
|
});
|
|
6468
8100
|
nestsApp.use("/:nestId/nodes/:nodeId{.+}", async (c, next) => {
|
|
@@ -6476,6 +8108,13 @@ function createApp() {
|
|
|
6476
8108
|
nestsApp.route("/:nestId/nodes", nodeRoutes);
|
|
6477
8109
|
nestsApp.route("/:nestId", queryRoutes);
|
|
6478
8110
|
nestsApp.route("/:nestId", sharingRoutes);
|
|
8111
|
+
nestsApp.route("/:nestId/grants", grantRoutes);
|
|
8112
|
+
nestsApp.route("/:nestId/edge-types", edgeTypeRoutes);
|
|
8113
|
+
nestsApp.route("/:nestId/edges", edgeRoutes);
|
|
8114
|
+
nestsApp.route("/:nestId/run", runTriggerRoutes);
|
|
8115
|
+
nestsApp.route("/:nestId/runs", runListRoutes);
|
|
8116
|
+
nestsApp.route("/:nestId/definitions", definitionRoutes);
|
|
8117
|
+
nestsApp.route("/:nestId/assets", assetRoutes);
|
|
6479
8118
|
nestsApp.route("/:nestId/mcp", mcpRoutes);
|
|
6480
8119
|
app.route("/nests", nestsApp);
|
|
6481
8120
|
app.use("/assets/*", serveStatic({ root: UI_DIR_REL }));
|