kibi-mcp 3.4.0 → 3.5.1

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
- // implements REQ-002
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 intentionally left degrading: its
164
- // required-guard branches carry no properties and would loosen input
165
- // validation instead of tightening it.
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.4.0",
3
+ "version": "3.5.1",
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.7.0",
13
+ "kibi-runtime": "^2.9.0",
14
14
  "kibi-core": "^0.17.0",
15
15
  "zod": "^4.3.6"
16
16
  },