kritya 0.8.11-beta → 0.8.13-beta

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.
@@ -7,6 +7,13 @@ import { McpAuthRequiredError } from "./oauth.js";
7
7
  import { missingVars } from "./servers.js";
8
8
  import { HttpTransport, StdioTransport } from "./transport.js";
9
9
  import { isPrivateOrLoopbackHost } from "../net/urlSafety.js";
10
+ import { checkToolsShape, serverFingerprint } from "../trust/mcpTrust.js";
11
+ /**
12
+ * Thrown when a server's live tool shape no longer matches what was recorded
13
+ * the first time its config was trusted — see checkToolsShape.
14
+ */
15
+ class McpShapeChangedError extends Error {
16
+ }
10
17
  /**
11
18
  * Model Context Protocol client with two transports and no SDK dependency
12
19
  * (to keep the install lean):
@@ -716,6 +723,9 @@ export async function connectServer(name, cfg, trace) {
716
723
  });
717
724
  const listed = await conn.initialize();
718
725
  const specs = listed.tools;
726
+ const shape = checkToolsShape(serverFingerprint(cfg), specs, trace?.mcpTrustFile);
727
+ if (!shape.ok)
728
+ throw new McpShapeChangedError(name);
719
729
  connections.push(conn);
720
730
  // Reconnects re-derive names from scratch; releasing the old claims first
721
731
  // keeps a server from colliding with its own previous incarnation.
@@ -747,6 +757,19 @@ export async function connectServer(name, cfg, trace) {
747
757
  span.setAttribute("kritya.mcp_needs_auth", true);
748
758
  span.setStatus("OK");
749
759
  }
760
+ else if (err instanceof McpShapeChangedError) {
761
+ status.shapeChanged = true;
762
+ status.error =
763
+ `its tools no longer match what you approved — run "/mcp trust revoke ${name}" ` +
764
+ `and restart kritya to review and re-approve it`;
765
+ span.setAttribute("kritya.mcp_shape_changed", true);
766
+ span.setStatus("ERROR", "tool shape changed");
767
+ trace?.audit?.logTool({
768
+ tool: "mcp_connect",
769
+ summary: `server "${name}" tool shape changed since approval`,
770
+ outcome: "error",
771
+ });
772
+ }
750
773
  else {
751
774
  status.error = err instanceof Error ? err.message : String(err);
752
775
  process.stderr.write(`kritya: MCP server "${name}" failed to start: ${status.error}\n`);
@@ -41,6 +41,17 @@ export function serverFingerprint(cfg) {
41
41
  };
42
42
  return crypto.createHash("sha256").update(JSON.stringify(shape)).digest("hex");
43
43
  }
44
+ /** Stable identity for a server's live tool list — what it actually does, not where it runs. */
45
+ export function toolsShapeHash(tools) {
46
+ const shape = tools
47
+ .map((t) => ({
48
+ name: t.name,
49
+ description: t.description ?? "",
50
+ inputSchema: t.inputSchema ?? null,
51
+ }))
52
+ .sort((a, b) => a.name.localeCompare(b.name));
53
+ return crypto.createHash("sha256").update(JSON.stringify(shape)).digest("hex");
54
+ }
44
55
  function loadStore(storeFile) {
45
56
  try {
46
57
  const parsed = JSON.parse(fs.readFileSync(storeFile, "utf8"));
@@ -109,3 +120,29 @@ export function partitionByTrust(servers, storeFile = MCP_TRUST_FILE) {
109
120
  }
110
121
  return { trusted, pending };
111
122
  }
123
+ /**
124
+ * Approving a server's *config* (command, url, env keys, ...) only promises
125
+ * the user reviewed where it runs and what it can reach — not what tools it
126
+ * actually exposes to the model, which a compromised or malicious server can
127
+ * change on any connection without touching its declared config at all. This
128
+ * compares a freshly-connected server's live tool shape (names, descriptions,
129
+ * input schemas) against what was recorded the first time its config was
130
+ * trusted.
131
+ *
132
+ * There is nothing to compare on that first connection (or after upgrading
133
+ * from a version that didn't record this), so it's recorded rather than
134
+ * flagged — otherwise every existing approval would start failing the moment
135
+ * this check shipped.
136
+ */
137
+ export function checkToolsShape(fingerprint, tools, storeFile = MCP_TRUST_FILE) {
138
+ const entries = loadStore(storeFile);
139
+ const entry = entries.find((e) => e.fingerprint === fingerprint);
140
+ if (!entry)
141
+ return { ok: true, recorded: false };
142
+ const hash = toolsShapeHash(tools);
143
+ if (!entry.toolsHash) {
144
+ saveStore(storeFile, entries.map((e) => (e === entry ? { ...e, toolsHash: hash } : e)));
145
+ return { ok: true, recorded: true };
146
+ }
147
+ return { ok: entry.toolsHash === hash, recorded: false };
148
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kritya",
3
- "version": "0.8.11-beta",
3
+ "version": "0.8.13-beta",
4
4
  "description": "Kritya — a lean, provider-agnostic terminal coding agent (NVIDIA, OpenAI, OpenRouter, Groq, Ollama, and more)",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -61,30 +61,30 @@
61
61
  "ink": "^7.1.1",
62
62
  "ink-text-input": "^6.0.0",
63
63
  "jszip": "^3.10.1",
64
- "mammoth": "^1.12.1",
65
- "openai": "^7.5.0",
64
+ "mammoth": "^1.12.2",
65
+ "openai": "^7.8.0",
66
66
  "pdf-lib": "^1.17.1",
67
- "pdfjs-dist": "^6.1.200",
67
+ "pdfjs-dist": "^6.3.289",
68
68
  "pptxgenjs": "^4.0.1",
69
69
  "react": "^19.2.8",
70
70
  "string-width": "^8.2.2"
71
71
  },
72
72
  "devDependencies": {
73
73
  "@eslint/js": "^10.0.1",
74
- "@types/node": "^26.2.0",
74
+ "@types/node": "^26.4.0",
75
75
  "@types/react": "^19.2.18",
76
76
  "c8": "^12.0.0",
77
77
  "electron": "^43.4.1",
78
78
  "electron-builder": "^26.15.3",
79
- "eslint": "^10.8.1",
79
+ "eslint": "^10.9.1",
80
80
  "globals": "^17.11.0",
81
81
  "husky": "^9.1.7",
82
82
  "ink-testing-library": "^4.0.0",
83
- "lint-staged": "^17.3.0",
83
+ "lint-staged": "^17.4.1",
84
84
  "prettier": "^3.3.0",
85
85
  "tsx": "^4.23.9",
86
86
  "typescript": "^6.0.3",
87
- "typescript-eslint": "^8.0.0"
87
+ "typescript-eslint": "^8.68.0"
88
88
  },
89
89
  "overrides": {
90
90
  "brace-expansion": "^5.0.8",