@grainulation/silo 1.0.4 → 1.1.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/lib/import-export.js +32 -1
- package/lib/packs.js +14 -1
- package/lib/serve-mcp.js +34 -3
- package/package.json +1 -1
package/lib/import-export.js
CHANGED
|
@@ -9,10 +9,21 @@ const fs = require("node:fs");
|
|
|
9
9
|
const path = require("node:path");
|
|
10
10
|
const { Store } = require("./store.js");
|
|
11
11
|
|
|
12
|
+
const VALID_TYPES = [
|
|
13
|
+
"constraint",
|
|
14
|
+
"factual",
|
|
15
|
+
"estimate",
|
|
16
|
+
"risk",
|
|
17
|
+
"recommendation",
|
|
18
|
+
"feedback",
|
|
19
|
+
];
|
|
20
|
+
const VALID_EVIDENCE = ["stated", "web", "documented", "tested", "production"];
|
|
21
|
+
|
|
12
22
|
/**
|
|
13
23
|
* Normalize a claim to wheat's canonical schema.
|
|
14
24
|
* Handles legacy fields: tier -> evidence, text -> content.
|
|
15
25
|
* Fills in missing required fields with sensible defaults.
|
|
26
|
+
* Validates enums, sanitizes content, and ensures provenance.
|
|
16
27
|
*/
|
|
17
28
|
function normalizeClaim(claim) {
|
|
18
29
|
const normalized = { ...claim };
|
|
@@ -41,6 +52,21 @@ function normalizeClaim(claim) {
|
|
|
41
52
|
normalized.resolved_by = normalized.resolved_by || null;
|
|
42
53
|
normalized.tags = normalized.tags || [];
|
|
43
54
|
|
|
55
|
+
// Enum validation: clamp type to allowed set
|
|
56
|
+
if (!VALID_TYPES.includes(normalized.type)) {
|
|
57
|
+
normalized.type = "factual";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Enum validation: clamp evidence to allowed set
|
|
61
|
+
if (!VALID_EVIDENCE.includes(normalized.evidence)) {
|
|
62
|
+
normalized.evidence = "stated";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// String sanitization: strip HTML tags from content
|
|
66
|
+
if (typeof normalized.content === "string") {
|
|
67
|
+
normalized.content = normalized.content.replace(/<[^>]*>/g, "");
|
|
68
|
+
}
|
|
69
|
+
|
|
44
70
|
// Normalize source to object form
|
|
45
71
|
if (!normalized.source || typeof normalized.source === "string") {
|
|
46
72
|
normalized.source = {
|
|
@@ -53,6 +79,11 @@ function normalizeClaim(claim) {
|
|
|
53
79
|
};
|
|
54
80
|
}
|
|
55
81
|
|
|
82
|
+
// Ensure provenance: if source.origin is missing, set default
|
|
83
|
+
if (!normalized.source.origin) {
|
|
84
|
+
normalized.source.origin = "silo-import";
|
|
85
|
+
}
|
|
86
|
+
|
|
56
87
|
return normalized;
|
|
57
88
|
}
|
|
58
89
|
|
|
@@ -176,4 +207,4 @@ class ImportExport {
|
|
|
176
207
|
}
|
|
177
208
|
}
|
|
178
209
|
|
|
179
|
-
module.exports = { ImportExport, normalizeClaim };
|
|
210
|
+
module.exports = { ImportExport, normalizeClaim, VALID_TYPES, VALID_EVIDENCE };
|
package/lib/packs.js
CHANGED
|
@@ -140,7 +140,20 @@ class Packs {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
const pack = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
143
|
-
|
|
143
|
+
|
|
144
|
+
// Validate pack name against path traversal
|
|
145
|
+
const rawName = pack.name || path.basename(filePath, ".json");
|
|
146
|
+
if (
|
|
147
|
+
/[/\\]/.test(rawName) ||
|
|
148
|
+
rawName.includes("..") ||
|
|
149
|
+
rawName.startsWith(".")
|
|
150
|
+
) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Invalid pack name "${rawName}": must not contain /, \\, .., or start with .`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const slug = rawName
|
|
144
157
|
.toLowerCase()
|
|
145
158
|
.replace(/[^a-z0-9]+/g, "-")
|
|
146
159
|
.replace(/^-|-$/g, "");
|
package/lib/serve-mcp.js
CHANGED
|
@@ -108,7 +108,14 @@ function toolPull(dir, args) {
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
const targetFile = into
|
|
111
|
+
const targetFile = into ? path.resolve(dir, into) : path.join(dir, "claims.json");
|
|
112
|
+
// Prevent path traversal — target must stay within workspace
|
|
113
|
+
if (targetFile !== dir && !targetFile.startsWith(dir + path.sep)) {
|
|
114
|
+
return {
|
|
115
|
+
status: "error",
|
|
116
|
+
message: `Target path escapes workspace directory.`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
112
119
|
if (!fs.existsSync(targetFile)) {
|
|
113
120
|
return {
|
|
114
121
|
status: "error",
|
|
@@ -135,7 +142,14 @@ function toolStore(dir, args) {
|
|
|
135
142
|
return { status: "error", message: "Required field: name" };
|
|
136
143
|
}
|
|
137
144
|
|
|
138
|
-
const sourceFile = from
|
|
145
|
+
const sourceFile = from ? path.resolve(dir, from) : path.join(dir, "claims.json");
|
|
146
|
+
// Prevent path traversal — source must stay within workspace
|
|
147
|
+
if (sourceFile !== dir && !sourceFile.startsWith(dir + path.sep)) {
|
|
148
|
+
return {
|
|
149
|
+
status: "error",
|
|
150
|
+
message: `Source path escapes workspace directory.`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
139
153
|
if (!fs.existsSync(sourceFile)) {
|
|
140
154
|
return { status: "error", message: `Source file not found: ${sourceFile}` };
|
|
141
155
|
}
|
|
@@ -247,7 +261,14 @@ function toolConfluence(dir, args) {
|
|
|
247
261
|
case "publish": {
|
|
248
262
|
const { title, from, spaceKey, parentId, pageId } = args;
|
|
249
263
|
if (!title) return { status: "error", message: "Required field: title" };
|
|
250
|
-
const sourceFile = from
|
|
264
|
+
const sourceFile = from ? path.resolve(dir, from) : path.join(dir, "claims.json");
|
|
265
|
+
// Prevent path traversal — source must stay within workspace
|
|
266
|
+
if (sourceFile !== dir && !sourceFile.startsWith(dir + path.sep)) {
|
|
267
|
+
return {
|
|
268
|
+
status: "error",
|
|
269
|
+
message: `Source path escapes workspace directory.`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
251
272
|
if (!fs.existsSync(sourceFile))
|
|
252
273
|
return {
|
|
253
274
|
status: "error",
|
|
@@ -269,6 +290,16 @@ function toolConfluence(dir, args) {
|
|
|
269
290
|
const target = pid || searchTitle;
|
|
270
291
|
if (!target)
|
|
271
292
|
return { status: "error", message: "Required: pageId or title" };
|
|
293
|
+
// Prevent path traversal — target must stay within workspace
|
|
294
|
+
if (into) {
|
|
295
|
+
const checkPath = path.resolve(dir, into);
|
|
296
|
+
if (checkPath !== dir && !checkPath.startsWith(dir + path.sep)) {
|
|
297
|
+
return {
|
|
298
|
+
status: "error",
|
|
299
|
+
message: `Target path escapes workspace directory.`,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
272
303
|
return confluence
|
|
273
304
|
.pull(target, { spaceKey })
|
|
274
305
|
.then((result) => {
|