kibi-mcp 3.4.0 → 3.5.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.
|
@@ -118,7 +118,68 @@ function conditionalRequiredKeys(value, condition) {
|
|
|
118
118
|
? required.filter((key) => typeof key === "string" && key.length > 0)
|
|
119
119
|
: [];
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
const GUARD_KEYWORDS = new Set([
|
|
122
|
+
"required",
|
|
123
|
+
"not",
|
|
124
|
+
"anyOf",
|
|
125
|
+
"allOf",
|
|
126
|
+
"description",
|
|
127
|
+
]);
|
|
128
|
+
// A guard branch only states which keys must or must not be present
|
|
129
|
+
// (required/not/anyOf/allOf of such conditions). It carries no property
|
|
130
|
+
// schemas, so enforcing a oneOf of guard branches can only tighten
|
|
131
|
+
// validation, never loosen it.
|
|
132
|
+
function isGuardBranch(branch) {
|
|
133
|
+
if (branch === null || typeof branch !== "object" || Array.isArray(branch))
|
|
134
|
+
return false;
|
|
135
|
+
const record = branch;
|
|
136
|
+
const keys = Object.keys(record);
|
|
137
|
+
if (keys.length === 0)
|
|
138
|
+
return false;
|
|
139
|
+
return keys.every((key) => {
|
|
140
|
+
if (!GUARD_KEYWORDS.has(key))
|
|
141
|
+
return false;
|
|
142
|
+
const entry = record[key];
|
|
143
|
+
if (key === "required")
|
|
144
|
+
return (Array.isArray(entry) && entry.every((item) => typeof item === "string"));
|
|
145
|
+
if (key === "not")
|
|
146
|
+
return isGuardBranch(entry);
|
|
147
|
+
if (key === "anyOf" || key === "allOf")
|
|
148
|
+
return Array.isArray(entry) && entry.every(isGuardBranch);
|
|
149
|
+
return true;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function guardOneOfBranches(obj) {
|
|
153
|
+
if (!Array.isArray(obj.oneOf) || obj.oneOf.length < 2)
|
|
154
|
+
return [];
|
|
155
|
+
return obj.oneOf.every(isGuardBranch) ? obj.oneOf : [];
|
|
156
|
+
}
|
|
157
|
+
function describeGuardBranch(branch) {
|
|
158
|
+
const required = Array.isArray(branch.required)
|
|
159
|
+
? branch.required
|
|
160
|
+
: [];
|
|
161
|
+
const forbidden = new Set();
|
|
162
|
+
const collect = (entry) => {
|
|
163
|
+
if (entry === null || typeof entry !== "object")
|
|
164
|
+
return;
|
|
165
|
+
const record = entry;
|
|
166
|
+
if (Array.isArray(record.required))
|
|
167
|
+
for (const key of record.required)
|
|
168
|
+
forbidden.add(key);
|
|
169
|
+
if (Array.isArray(record.anyOf))
|
|
170
|
+
for (const item of record.anyOf)
|
|
171
|
+
collect(item);
|
|
172
|
+
if (Array.isArray(record.allOf))
|
|
173
|
+
for (const item of record.allOf)
|
|
174
|
+
collect(item);
|
|
175
|
+
};
|
|
176
|
+
collect(branch.not);
|
|
177
|
+
const parts = [`{${required.join(", ")}}`];
|
|
178
|
+
if (forbidden.size > 0)
|
|
179
|
+
parts.push(`without ${[...forbidden].join(", ")}`);
|
|
180
|
+
return parts.join(" ");
|
|
181
|
+
}
|
|
182
|
+
// implements REQ-002, REQ-mcp-oneof-guard-input-validation
|
|
122
183
|
export function jsonSchemaToZod(schema) {
|
|
123
184
|
if (!schema || typeof schema !== "object") {
|
|
124
185
|
return z.any();
|
|
@@ -160,9 +221,10 @@ export function jsonSchemaToZod(schema) {
|
|
|
160
221
|
// JSON Schema anyOf unions become real Zod unions so MCP output
|
|
161
222
|
// validation enforces the declared alternatives instead of silently
|
|
162
223
|
// degrading to z.any() — e.g. kb_check's synchronous payload versus its
|
|
163
|
-
// kibi.job.v1 async receipt. oneOf is
|
|
164
|
-
// required-guard branches carry no properties and would loosen
|
|
165
|
-
// validation
|
|
224
|
+
// kibi.job.v1 async receipt. oneOf is not turned into a union: its
|
|
225
|
+
// required-guard branches carry no properties and a union would loosen
|
|
226
|
+
// input validation. Guard oneOf branches on an object are enforced as a
|
|
227
|
+
// refinement in the object case below instead.
|
|
166
228
|
const anyOfVariants = Array.isArray(obj.anyOf) ? obj.anyOf : [];
|
|
167
229
|
if (anyOfVariants.length > 0) {
|
|
168
230
|
const variants = anyOfVariants.map((variant) => jsonSchemaToZod(variant));
|
|
@@ -218,6 +280,27 @@ export function jsonSchemaToZod(schema) {
|
|
|
218
280
|
})
|
|
219
281
|
.meta({ allOf: obj.allOf });
|
|
220
282
|
}
|
|
283
|
+
// A top-level oneOf of guard branches (for example "plan with
|
|
284
|
+
// approvedPlanHash, or recoveryJournalId alone") is enforced with the
|
|
285
|
+
// same condition matcher as if/then: exactly one branch must match.
|
|
286
|
+
// It is not republished as JSON Schema metadata, because hosts reject
|
|
287
|
+
// a top-level oneOf in a tool input schema; the CLI enforces the same
|
|
288
|
+
// rule with ajv.
|
|
289
|
+
const guardBranches = guardOneOfBranches(obj);
|
|
290
|
+
if (guardBranches.length > 0) {
|
|
291
|
+
result = result.superRefine((value, context) => {
|
|
292
|
+
if (value === null || typeof value !== "object")
|
|
293
|
+
return;
|
|
294
|
+
const matches = guardBranches.filter((branch) => matchesJsonSchemaCondition(value, branch)).length;
|
|
295
|
+
if (matches === 1)
|
|
296
|
+
return;
|
|
297
|
+
context.addIssue({
|
|
298
|
+
code: "custom",
|
|
299
|
+
path: [],
|
|
300
|
+
message: `Input must match exactly one of: ${guardBranches.map(describeGuardBranch).join("; ")} (matched ${matches}).`,
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
221
304
|
return result;
|
|
222
305
|
}
|
|
223
306
|
case "array": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getSpec, statusSpec } from "kibi-runtime";
|
|
2
|
-
import { MODEL_ROUTES, SKILL_ROUTES, dispatchComposite, executeApplyPlan, executeCompileIntent, executeDelete, executeIngestProof, executeUpsert, } from "kibi-runtime";
|
|
2
|
+
import { MODEL_ROUTES, SKILL_ROUTES, dispatchComposite, executeApplyPlan, executeCompileIntent, executeDelete, executeIngestProof, executeUpsert, preflightApplyPlan, } from "kibi-runtime";
|
|
3
3
|
import { OPTIONAL_TOOL_NAMES, enabledOptionalTools } from "../tools-config.js";
|
|
4
4
|
import { startJob } from "./jobs.js";
|
|
5
5
|
// implements REQ-002, REQ-013
|
|
@@ -165,6 +165,10 @@ export function registerConfiguredTools(server, runtime, registerTool) {
|
|
|
165
165
|
// Detach it into a job and return the kibi.job.v1 receipt; the agent
|
|
166
166
|
// polls kb_job_status. The request is over once the receipt returns,
|
|
167
167
|
// so the job reports no progress and refreshes the branch stamp itself.
|
|
168
|
+
// Input and stale-plan errors (missing or wrong approvedPlanHash, a
|
|
169
|
+
// changed snapshot) fail this call rather than the job; the apply
|
|
170
|
+
// repeats every check under the workspace lock.
|
|
171
|
+
await (runtime.preflightApplyPlan ?? preflightApplyPlan)(applyArgs, context);
|
|
168
172
|
const { onProgress: _requestProgress, ...jobContext } = context;
|
|
169
173
|
return startJob("kb_apply_plan", async () => {
|
|
170
174
|
const result = await apply(jobContext);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"mcpName": "io.github.Looted/kibi-mcp",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"fast-glob": "^3.2.12",
|
|
11
11
|
"gray-matter": "^4.0.3",
|
|
12
12
|
"js-yaml": "^4.1.0",
|
|
13
|
-
"kibi-runtime": "^2.
|
|
13
|
+
"kibi-runtime": "^2.8.0",
|
|
14
14
|
"kibi-core": "^0.17.0",
|
|
15
15
|
"zod": "^4.3.6"
|
|
16
16
|
},
|