glasspane-mcp 1.4.0 → 1.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.
@@ -26,7 +26,7 @@ export declare const MCP_PROTOCOL_VERSION: "2025-06-18";
26
26
  export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
27
27
  export declare const SERVER_INFO: {
28
28
  readonly name: "glasspane-mcp";
29
- readonly version: "1.4.0";
29
+ readonly version: "1.5.0";
30
30
  };
31
31
  export declare const PARSE_ERROR = -32700;
32
32
  export declare const INVALID_REQUEST = -32600;
@@ -1300,6 +1300,10 @@ function sortKeys(value) {
1300
1300
  return value;
1301
1301
  }
1302
1302
 
1303
+ // src/update-state.ts
1304
+ var CA_ROOTS_STATUSES = ["ok", "probe-failed", "empty", "unavailable", "write-unverified"];
1305
+ var KNOWN_CA_ROOTS_STATUSES = new Set(CA_ROOTS_STATUSES);
1306
+
1303
1307
  // src/project-registry.ts
1304
1308
  import fs2 from "node:fs";
1305
1309
  import os2 from "node:os";
@@ -1290,6 +1290,10 @@ function sortKeys(value) {
1290
1290
  return value;
1291
1291
  }
1292
1292
 
1293
+ // src/update-state.ts
1294
+ var CA_ROOTS_STATUSES = ["ok", "probe-failed", "empty", "unavailable", "write-unverified"];
1295
+ var KNOWN_CA_ROOTS_STATUSES = new Set(CA_ROOTS_STATUSES);
1296
+
1293
1297
  // src/project-registry.ts
1294
1298
  import fs2 from "node:fs";
1295
1299
  import os from "node:os";
package/dist/index.js CHANGED
@@ -304,6 +304,69 @@ var KNOWN_STATUSES = /* @__PURE__ */ new Set([
304
304
  "disabled",
305
305
  "installer-pointer-stale"
306
306
  ]);
307
+ var CA_ROOTS_STATUSES = ["ok", "probe-failed", "empty", "unavailable", "write-unverified"];
308
+ var KNOWN_CA_ROOTS_STATUSES = new Set(CA_ROOTS_STATUSES);
309
+ function isCaRootsStatus(value) {
310
+ return KNOWN_CA_ROOTS_STATUSES.has(value);
311
+ }
312
+ var CA_BUNDLE_FILE_NAME = "ca-roots.pem";
313
+ function decodeCaRoots(value, bundlePath) {
314
+ if (value === void 0 || value === null) {
315
+ return { ok: true, ca: { recorded: false, bundlePath } };
316
+ }
317
+ if (typeof value !== "object" || Array.isArray(value)) {
318
+ return {
319
+ ok: false,
320
+ reason: `caRoots is ${Array.isArray(value) ? "an array" : typeof value} instead of the object the exporter writes`
321
+ };
322
+ }
323
+ const record = value;
324
+ const rawStatus = record.status;
325
+ if (typeof rawStatus !== "string" || !isCaRootsStatus(rawStatus)) {
326
+ return {
327
+ ok: false,
328
+ reason: `caRoots.status ${JSON.stringify(rawStatus ?? null)} is not one of ${CA_ROOTS_STATUSES.join(", ")}`
329
+ };
330
+ }
331
+ const rawCerts = record.certs;
332
+ let certs = null;
333
+ if (typeof rawCerts === "number") {
334
+ if (!Number.isInteger(rawCerts) || rawCerts < 0) {
335
+ return { ok: false, reason: `caRoots.certs ${JSON.stringify(rawCerts)} is not a certificate count` };
336
+ }
337
+ certs = rawCerts;
338
+ } else if (rawCerts !== void 0 && rawCerts !== null) {
339
+ return { ok: false, reason: `caRoots.certs is ${typeof rawCerts} instead of a count or null` };
340
+ }
341
+ if (rawStatus === "ok" && (certs === null || certs < 1)) {
342
+ return {
343
+ ok: false,
344
+ reason: `caRoots.status says ok but it records ${JSON.stringify(rawCerts ?? null)} certificates`
345
+ };
346
+ }
347
+ const rawPath = record.path;
348
+ let recordedPath = null;
349
+ if (typeof rawPath === "string") {
350
+ if (rawPath.trim() === "") {
351
+ return { ok: false, reason: "caRoots.path is blank, so the bundle it points at cannot be named" };
352
+ }
353
+ recordedPath = rawPath;
354
+ } else if (rawPath !== void 0 && rawPath !== null) {
355
+ return { ok: false, reason: `caRoots.path is ${typeof rawPath} instead of a path or null` };
356
+ }
357
+ return {
358
+ ok: true,
359
+ ca: {
360
+ recorded: true,
361
+ status: rawStatus,
362
+ certs,
363
+ exportedAt: typeof record.exportedAt === "string" ? record.exportedAt : null,
364
+ detail: typeof record.detail === "string" && record.detail.trim() !== "" ? record.detail : null,
365
+ bundlePath,
366
+ recordedPath
367
+ }
368
+ };
369
+ }
307
370
  function readUpdateState(env = process.env, stateRootOverride) {
308
371
  const stateFile = updateStateFile(env, stateRootOverride);
309
372
  if (stateFile === null) {
@@ -348,6 +411,14 @@ function readUpdateState(env = process.env, stateRootOverride) {
348
411
  summary: `update state: ${stateFile} carries status ${JSON.stringify(parsed.status ?? null)}, which is not a status this shell knows; the updater and this shell disagree about the contract, so freshness is unknown`
349
412
  };
350
413
  }
414
+ const ca = decodeCaRoots(parsed.caRoots, path.join(path.dirname(stateFile), CA_BUNDLE_FILE_NAME));
415
+ if (!ca.ok) {
416
+ return {
417
+ kind: "unreadable",
418
+ stateFile,
419
+ summary: `update state: ${stateFile} carries a CA trust bundle record this shell cannot read (${ca.reason}); whether this install can reach the release endpoint at all is unknown, so nothing below it is trusted either \u2014 re-run the installer or \`node updater/cli.js enable\` to write the record again`
420
+ };
421
+ }
351
422
  const bits = [
352
423
  `status=${status}`,
353
424
  `current=${String(parsed.current ?? "unknown")}`,
@@ -363,7 +434,7 @@ function readUpdateState(env = process.env, stateRootOverride) {
363
434
  if (staged && typeof staged.version === "string") {
364
435
  bits.push(`staged=${staged.version}${staged.digest ? ` (${String(staged.digest).slice(0, 12)}\u2026)` : ""}`);
365
436
  }
366
- return { kind: "read", stateFile, summary: `update state: ${bits.join("; ")}` };
437
+ return { kind: "read", stateFile, summary: `update state: ${bits.join("; ")}`, ca: ca.ca };
367
438
  }
368
439
 
369
440
  // src/io.ts
@@ -2838,7 +2909,12 @@ async function runValidatedTool(spec, value, engine, session, turn) {
2838
2909
  }
2839
2910
  const content = [{ type: "text", text: canonicalJson(raw) }];
2840
2911
  if (spec.name === "gp_diagnose") {
2841
- content.push({ type: "text", text: readUpdateState().summary });
2912
+ const reading = readUpdateState();
2913
+ content.push({
2914
+ type: "text",
2915
+ text: reading.kind === "read" ? `${reading.summary}
2916
+ ${caBundleDiagnosis(reading.ca)}` : reading.summary
2917
+ });
2842
2918
  }
2843
2919
  return {
2844
2920
  content,
@@ -2865,6 +2941,43 @@ async function runValidatedTool(spec, value, engine, session, turn) {
2865
2941
  };
2866
2942
  }
2867
2943
  }
2944
+ function caBundleDiagnosis(ca) {
2945
+ const enableCmd = "`node updater/cli.js enable`";
2946
+ if (!ca.recorded) {
2947
+ return `ca trust bundle: this install has never recorded exporting the machine's trusted roots (the state file carries no caRoots), which is NOT the same as "the bundle is fine" \u2014 an install predating the export step looks exactly like this. node then verifies only against the CA list inside its own binary, so on a machine whose HTTPS is re-signed locally every auto-update run fails certificate verification and nothing on the surface says so. Remedy: re-run the GlassPane installer, or ${enableCmd}, either of which writes the roots to ${ca.bundlePath} and points the launchd agent and the panel at it. For a check run by hand in a terminal: export NODE_EXTRA_CA_CERTS=${ca.bundlePath} \u2014 but only once that file exists, and set before node starts, because a running process never picks the variable up.`;
2948
+ }
2949
+ const target = ca.recordedPath ?? ca.bundlePath;
2950
+ const countClause = ca.certs === null ? "an unrecorded number of certificates" : `${ca.certs} certificates`;
2951
+ let diagnosis;
2952
+ switch (ca.status) {
2953
+ case "ok":
2954
+ diagnosis = `ca trust bundle: ok \u2014 node will use this exported root bundle (${countClause} at ${target}) for the updater's HTTPS, so a locally re-signed certificate chain on this machine does not stop a check. Nothing to do; the bundle is refreshed by the installer, by ${enableCmd}, and after every successful apply.`;
2955
+ break;
2956
+ case "probe-failed":
2957
+ diagnosis = `ca trust bundle: probe-failed \u2014 ${countClause} were written to ${target}, and reaching the release endpoint through them still failed. This is the machine-still-intercepted case: the exported bundle does not contain the root that re-signs this network's TLS, so the scheduled updater is dead on every run exactly as it was before the bundle existed. Remedy: get that root into the System keychain (/Library/Keychains/System.keychain), then refresh the export with ${enableCmd} or a re-run of the installer. Pointing NODE_EXTRA_CA_CERTS at ${target} changes nothing while the signing root is missing.`;
2958
+ break;
2959
+ case "empty": {
2960
+ const kept = ca.recordedPath !== null;
2961
+ diagnosis = `ca trust bundle: empty \u2014 "security find-certificate" ran and produced ${countClause}, so this export published nothing new` + (kept ? `; the bundle already at ${target} was kept and is still what the agent and the panel hand to node, so a check can pass today while this machine's current roots go unrecorded.` : `; nothing is at ${target} either, so on a machine whose HTTPS is re-signed locally every check fails certificate verification from here.`) + ` Remedy: check that both keychains are readable (\`security dump-keychain\` lists them), then re-run the GlassPane installer or ${enableCmd} to export again.`;
2962
+ break;
2963
+ }
2964
+ case "unavailable":
2965
+ diagnosis = `ca trust bundle: unavailable \u2014 the \`security\` tool was not found on this host, so this machine's trusted roots could not be read and no bundle was written or refreshed at ${target}. Unless something else already put a bundle there, the updater carries only node's built-in list, so on a re-signing network it cannot reach a release at all. Remedy: place a root bundle you trust at a path of your choosing and give that same path to both consumers \u2014 \`export NODE_EXTRA_CA_CERTS=<that path>\` for runs from this terminal, the launchd environment for the scheduled one \u2014 then re-run the GlassPane installer so the agent and the panel record it.`;
2966
+ break;
2967
+ case "write-unverified":
2968
+ diagnosis = `ca trust bundle: write-unverified \u2014 the bytes read back from ${target} were not the bytes the exporter wrote, and the export counted ${countClause}, so whatever sits at that path is not the bundle this install believes in. Verification can fail at any point, including after a partial write. Remedy: re-run the GlassPane installer or ${enableCmd}, which exports again and reads the file back; if it repeats, ${target} sits on a volume that will not hold a private regular file, and auto-update must stay off until that is fixed.`;
2969
+ break;
2970
+ default: {
2971
+ const unhandled = ca.status;
2972
+ diagnosis = `ca trust bundle: the state file records ${JSON.stringify(unhandled)}, a state this shell has no sentence for; treat the CA trust bundle as unverified and re-run the GlassPane installer`;
2973
+ }
2974
+ }
2975
+ const provenance = [
2976
+ ca.exportedAt === null ? null : `exported ${ca.exportedAt}`,
2977
+ ca.detail === null ? null : `recorded reason: ${ca.detail}`
2978
+ ].filter((part) => part !== null);
2979
+ return provenance.length === 0 ? diagnosis : `${diagnosis} The record itself says: ${provenance.join("; ")}.`;
2980
+ }
2868
2981
  var EvidenceFrameShapeError = class extends Error {
2869
2982
  constructor(detail) {
2870
2983
  super(`engine returned a last_evidence frame without an evidencePack object: ${detail}`);
@@ -3325,7 +3438,7 @@ var SUPPORTED_PROTOCOL_VERSIONS = [
3325
3438
  "2025-03-26",
3326
3439
  MCP_PROTOCOL_VERSION
3327
3440
  ];
3328
- var SERVER_INFO = { name: "glasspane-mcp", version: "1.4.0" };
3441
+ var SERVER_INFO = { name: "glasspane-mcp", version: "1.5.0" };
3329
3442
  var PARSE_ERROR = -32700;
3330
3443
  var INVALID_REQUEST = -32600;
3331
3444
  var METHOD_NOT_FOUND = -32601;
@@ -11,11 +11,46 @@ export declare const STATE_DIR_ENV = "GLASSPANE_STATE_DIR";
11
11
  export declare function recordHome(): string | null;
12
12
  /** Where `update-state.json` lives for this process, or null when it cannot be named. */
13
13
  export declare function updateStateFile(env?: NodeJS.ProcessEnv, stateRootOverride?: string): string | null;
14
- export interface UpdateStateReading {
15
- kind: "read" | "absent" | "unreadable" | "unresolvable";
14
+ export type UpdateStateReading = {
15
+ kind: "read";
16
+ stateFile: string;
17
+ summary: string;
18
+ /** Decoded `caRoots`, present exactly when the file was read and understood. */
19
+ ca: CaRootsReading;
20
+ } | {
21
+ kind: "absent" | "unreadable" | "unresolvable";
16
22
  stateFile: string | null;
17
23
  summary: string;
18
- }
24
+ };
25
+ /**
26
+ * The five states the exporter can record for the machine's root bundle, as a closed
27
+ * set: `ok` (a non-empty bundle landed and a probe through it reached the release
28
+ * endpoint), `probe-failed` (bundle landed, endpoint still unreachable with it),
29
+ * `empty` (the keychain dump produced no certificates), `unavailable` (no keychain
30
+ * tool on this host), `write-unverified` (the file read back different from what was
31
+ * written). Only `ok` means usable; the other four each fail in their own way, so
32
+ * collapsing any of them into `ok` hands an agent a false all-clear.
33
+ */
34
+ export declare const CA_ROOTS_STATUSES: readonly ["ok", "probe-failed", "empty", "unavailable", "write-unverified"];
35
+ export type CaRootsStatus = (typeof CA_ROOTS_STATUSES)[number];
36
+ /** Name the exporter gives the bundle inside the state root; used to name it when the record does not. */
37
+ export declare const CA_BUNDLE_FILE_NAME = "ca-roots.pem";
38
+ export type CaRootsReading = {
39
+ recorded: true;
40
+ status: CaRootsStatus;
41
+ /** Certificate count as recorded; `null` only for the non-`ok` states, which may not have one. */
42
+ certs: number | null;
43
+ exportedAt: string | null;
44
+ /** The exporter's own reason text, when it wrote one. */
45
+ detail: string | null;
46
+ /** Where this state root's bundle lives, resolved the same way the state file was. */
47
+ bundlePath: string;
48
+ /** The path the exporter recorded, when it recorded one. */
49
+ recordedPath: string | null;
50
+ } | {
51
+ recorded: false;
52
+ bundlePath: string;
53
+ };
19
54
  /**
20
55
  * Read and render. Never throws: `gp_diagnose` answers about an operation, and a
21
56
  * missing or broken update file is a fact about the machine, not a reason to fail
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glasspane-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "GlassPane MCP stdio shell: JSON-RPC 2.0 over stdio bridging an AI agent to the glasspane engine daemon",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",