glasspane-mcp 1.22.0 → 1.24.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.22.0";
29
+ readonly version: "1.24.0";
30
30
  };
31
31
  export declare const PARSE_ERROR = -32700;
32
32
  export declare const INVALID_REQUEST = -32600;
@@ -590,6 +590,7 @@ export declare class EngineJsonRpcClient {
590
590
  private verifyIdentity;
591
591
  }
592
592
  export declare function truncate(text: string): string;
593
+ export declare function lateReplyBodyNote(serializedBody: string, operationId?: unknown): string;
593
594
  /** How a transport stream is opened; injectable so tests need no socket. */
594
595
  export type EngineTransportOpener = (socketPath: string) => Duplex;
595
596
  /**
@@ -8,6 +8,7 @@ import net from "node:net";
8
8
  import os from "node:os";
9
9
  import path from "node:path";
10
10
  import fs2 from "node:fs";
11
+ import { createHash } from "node:crypto";
11
12
  import { fileURLToPath } from "node:url";
12
13
 
13
14
  // src/io.ts
@@ -800,9 +801,9 @@ var EngineJsonRpcClient = class {
800
801
  );
801
802
  return;
802
803
  }
803
- const bodyBytes = Buffer.byteLength(jsonOrString(frame.result), "utf8");
804
+ const bodyJson = jsonOrString(frame.result);
804
805
  this.report(
805
- `late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: the engine did answer, with a ${bodyBytes}-byte body that is raw engine content (an accessibility tree whose title/value fields are on-screen data), so its text is not written to this log${byOrder}`
806
+ `late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: the engine did answer, ${lateReplyBodyNote(bodyJson, operationIdOf(frame.result))}${byOrder}`
806
807
  );
807
808
  if (frame.result === void 0) {
808
809
  return;
@@ -957,6 +958,13 @@ function attribute(error, entry, inferred = true) {
957
958
  function truncate(text) {
958
959
  return text.length <= LOG_BODY_CHARS ? text : `${text.slice(0, LOG_BODY_CHARS)}\u2026 [+${text.length - LOG_BODY_CHARS} chars not logged]`;
959
960
  }
961
+ var ARCHIVE_OPERATION_ID = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
962
+ function lateReplyBodyNote(serializedBody, operationId) {
963
+ const bytes = Buffer.byteLength(serializedBody, "utf8");
964
+ const digest = createHash("sha256").update(serializedBody, "utf8").digest("hex").slice(0, 12);
965
+ const named2 = typeof operationId === "string" && ARCHIVE_OPERATION_ID.test(operationId) ? `, for ${operationId}` : "";
966
+ return `with a ${bytes}-byte body (sha256 ${digest})${named2} that is on-screen accessibility content \u2014 element titles and values, sometimes a document's text \u2014 so its text is not written to this log; the reply itself is in the daemon's evidence archive under the state root, read it back with gp_last_evidence {operationId} or gp_export_evidence {operationId}`;
967
+ }
960
968
  function nonBlankOr(value, fallback) {
961
969
  if (value === void 0 || value === null) {
962
970
  return fallback;
@@ -967,6 +975,9 @@ function nonBlankOr(value, fallback) {
967
975
  function jsonOrString(value) {
968
976
  return String(JSON.stringify(value));
969
977
  }
978
+ function operationIdOf(result) {
979
+ return typeof result === "object" && result !== null ? result.operationId : void 0;
980
+ }
970
981
  var ReconnectingSocketIo = class {
971
982
  constructor(socketPath, open) {
972
983
  this.socketPath = socketPath;
@@ -1050,7 +1061,7 @@ function unixSocketEngineClient(socketPath, options = {}) {
1050
1061
 
1051
1062
  // src/http-gateway.ts
1052
1063
  import http from "node:http";
1053
- import { createHash as createHash2, timingSafeEqual } from "node:crypto";
1064
+ import { createHash as createHash3, timingSafeEqual } from "node:crypto";
1054
1065
  import { isIP } from "node:net";
1055
1066
  import { URL } from "node:url";
1056
1067
 
@@ -1140,6 +1151,9 @@ function engineBody(params) {
1140
1151
  const { dimension: _dimension, ...rest } = params;
1141
1152
  return rest;
1142
1153
  }
1154
+ function dimensionAttributionLine(dimension) {
1155
+ return `dimension: ${dimension} | declared by this call; this shell records no decision, so the ledger gains an attribution only where a decision is written for this operation`;
1156
+ }
1143
1157
 
1144
1158
  // src/evidence-report.ts
1145
1159
  var PLACEHOLDER = "\u2014";
@@ -2007,14 +2021,21 @@ function attributeCapability(engineMethod, query = {}) {
2007
2021
  };
2008
2022
  } catch (error) {
2009
2023
  if (error instanceof KernelProviderError) {
2010
- return { status: "refused", error: { code: error.code, message: error.message, remedy: error.remedy } };
2024
+ return {
2025
+ status: "refused",
2026
+ error: {
2027
+ code: GP_E_INTERNAL,
2028
+ message: `provider registration refused (${error.code}): ${error.message}`,
2029
+ remedy: error.remedy
2030
+ }
2031
+ };
2011
2032
  }
2012
2033
  if (error instanceof KernelSchemaError) {
2013
2034
  return {
2014
2035
  status: "refused",
2015
2036
  error: {
2016
- code: error.code,
2017
- message: `${error.message} \u2014 the kernel refused a registration this product writes itself, built with the version from ${registrationVersionSource(measured, usable(own))}`,
2037
+ code: GP_E_INTERNAL,
2038
+ message: `the kernel refused a registration this product writes itself (${error.code}): ${error.message} \u2014 built with the version from ${registrationVersionSource(measured, usable(own))}`,
2018
2039
  remedy: "fix shellProviderDrafts in mcp-shell/src/providers.ts; it is the contract both harnesses read, so it cannot be relaxed at a call site \u2014 run node scripts/check-kernel-corpus.mjs and compare against kernel/fixtures/provider-registration.ok-01.json"
2019
2040
  }
2020
2041
  };
@@ -2065,7 +2086,7 @@ var TRAIL_PROVENANCE_NOTES = {
2065
2086
  import fs4 from "node:fs";
2066
2087
  import os2 from "node:os";
2067
2088
  import path3 from "node:path";
2068
- import { createHash, randomBytes } from "node:crypto";
2089
+ import { createHash as createHash2, randomBytes } from "node:crypto";
2069
2090
  import { z as z9 } from "zod";
2070
2091
  var CROCKFORD_BODY_RE = "[0-9A-HJKMNP-TV-Z]{26}";
2071
2092
  var PROJECT_ID_RE = new RegExp(`^prj_${CROCKFORD_BODY_RE}$`);
@@ -2369,7 +2390,7 @@ function registryDigest(filePath) {
2369
2390
  return registryBytesDigest(bytes);
2370
2391
  }
2371
2392
  function registryBytesDigest(bytes) {
2372
- return createHash("sha256").update(bytes).digest("hex");
2393
+ return createHash2("sha256").update(bytes).digest("hex");
2373
2394
  }
2374
2395
  function registryDriftReason(filePath, openedAs, now) {
2375
2396
  if (now === openedAs) {
@@ -3848,6 +3869,7 @@ var HTTP_NOT_FOUND = "GP_HTTP_NOT_FOUND";
3848
3869
  var HTTP_BAD_REQUEST = "GP_HTTP_BAD_REQUEST";
3849
3870
  var HTTP_PAYLOAD_TOO_LARGE = "GP_HTTP_PAYLOAD_TOO_LARGE";
3850
3871
  var HTTP_NOT_PROVIDED = "GP_HTTP_NOT_PROVIDED";
3872
+ var HTTP_BODY_UNREAD = "GP_HTTP_BODY_UNREAD";
3851
3873
  var FORWARDABLE_TOOLS = new Map(
3852
3874
  TOOL_SPECS.filter((spec) => spec.execute === void 0).map((spec) => [spec.engineMethod, spec])
3853
3875
  );
@@ -3895,11 +3917,12 @@ function createHttpGateway(options) {
3895
3917
  const client = options.connectController !== void 0 ? options.connectController(options.socketPath) : unixSocketEngineClient(options.socketPath, { surface: "http" });
3896
3918
  client.onEngineNote?.((note) => report(`engine: ${note}`));
3897
3919
  client.onLateReply?.(({ method, result }) => {
3898
- report(`late engine reply to '${method}' (reply body below, raw engine content and not redacted): ${truncate(stringifyReply(result))}`);
3920
+ report(`late engine reply to '${method}': the engine did answer, ${lateReplyBodyNote(stringifyReply(result), result?.operationId)}`);
3899
3921
  });
3900
3922
  const expectedToken = Buffer.from(options.token, "utf8");
3923
+ const bodyReadTimeoutMs = options.bodyReadTimeoutMs ?? DEFAULT_BODY_READ_TIMEOUT_MS;
3901
3924
  const server = http.createServer((req, res) => {
3902
- handleHttp(req, res, client, expectedToken).catch((error) => {
3925
+ handleHttp(req, res, client, expectedToken, bodyReadTimeoutMs).catch((error) => {
3903
3926
  fail(res, 500, GP_E_INTERNAL, `internal gateway error: ${String(error)}`, gatewayFaultAdvice());
3904
3927
  });
3905
3928
  });
@@ -3979,7 +4002,7 @@ function stringifyReply(result) {
3979
4002
  return String(result);
3980
4003
  }
3981
4004
  }
3982
- async function handleHttp(req, res, client, expectedToken) {
4005
+ async function handleHttp(req, res, client, expectedToken, bodyReadTimeoutMs = DEFAULT_BODY_READ_TIMEOUT_MS) {
3983
4006
  if (!authorized(req, expectedToken)) {
3984
4007
  fail(
3985
4008
  res,
@@ -3996,7 +4019,7 @@ async function handleHttp(req, res, client, expectedToken) {
3996
4019
  return;
3997
4020
  }
3998
4021
  const segments = url.pathname.split("/").filter((part) => part !== "");
3999
- await dispatch(req, res, url, segments, client);
4022
+ await dispatch(req, res, url, segments, client, bodyReadTimeoutMs);
4000
4023
  }
4001
4024
  function parseRequestUrl(raw) {
4002
4025
  if (typeof raw !== "string" || raw === "") {
@@ -4008,7 +4031,7 @@ function parseRequestUrl(raw) {
4008
4031
  return null;
4009
4032
  }
4010
4033
  }
4011
- async function dispatch(req, res, url, segments, client) {
4034
+ async function dispatch(req, res, url, segments, client, bodyReadTimeoutMs = DEFAULT_BODY_READ_TIMEOUT_MS) {
4012
4035
  const [v1, resource, id] = segments;
4013
4036
  if (v1 !== "v1") {
4014
4037
  NotFound(res, url.pathname);
@@ -4033,11 +4056,11 @@ async function dispatch(req, res, url, segments, client) {
4033
4056
  }
4034
4057
  } else if (req.method === "POST") {
4035
4058
  if (resource === "tools" && segments.length === 3 && id !== void 0) {
4036
- await handleToolForward(id, req, client, res);
4059
+ await handleToolForward(id, req, client, res, bodyReadTimeoutMs);
4037
4060
  return;
4038
4061
  }
4039
4062
  if (resource === "recipes" && id === "validate" && segments.length === 3) {
4040
- await handleRecipeValidate(req, res);
4063
+ await handleRecipeValidate(req, res, bodyReadTimeoutMs);
4041
4064
  return;
4042
4065
  }
4043
4066
  }
@@ -4254,7 +4277,7 @@ async function handleEvidenceAggregate(url, client, res) {
4254
4277
  report: sections2.join("\n")
4255
4278
  });
4256
4279
  }
4257
- async function handleToolForward(name, req, client, res) {
4280
+ async function handleToolForward(name, req, client, res, bodyReadTimeoutMs = DEFAULT_BODY_READ_TIMEOUT_MS) {
4258
4281
  const spec = FORWARDABLE_TOOLS.get(name);
4259
4282
  if (spec === void 0) {
4260
4283
  fail(
@@ -4268,7 +4291,7 @@ async function handleToolForward(name, req, client, res) {
4268
4291
  }
4269
4292
  let body;
4270
4293
  try {
4271
- const text = await readBody(req, MAX_REQUEST_BODY_BYTES);
4294
+ const text = await readBody(req, MAX_REQUEST_BODY_BYTES, bodyReadTimeoutMs);
4272
4295
  body = JSON.parse(text);
4273
4296
  } catch (error) {
4274
4297
  if (error instanceof HTTPPayloadTooLargeError) {
@@ -4281,6 +4304,16 @@ async function handleToolForward(name, req, client, res) {
4281
4304
  );
4282
4305
  return;
4283
4306
  }
4307
+ if (error instanceof HTTPBodyUnreadError) {
4308
+ fail(
4309
+ res,
4310
+ 408,
4311
+ HTTP_BODY_UNREAD,
4312
+ `request body never completed: ${error.message}`,
4313
+ `send the whole body in one request (Content-Length must match what you write) and retry; this gateway waits ${bodyReadTimeoutMs}ms for one body before it answers this way`
4314
+ );
4315
+ return;
4316
+ }
4284
4317
  fail(
4285
4318
  res,
4286
4319
  400,
@@ -4334,7 +4367,7 @@ async function handleToolForward(name, req, client, res) {
4334
4367
  return;
4335
4368
  }
4336
4369
  }
4337
- ok(res, result, attribution);
4370
+ ok(res, result, attribution, declared.dimension);
4338
4371
  } catch (error) {
4339
4372
  await failEngine(res, error, spec, engineBody(checked.value));
4340
4373
  }
@@ -4348,9 +4381,9 @@ function handleEvidenceStats(res) {
4348
4381
  "run `glasspaned --evidence-stats` against the same --state-dir this socket serves to get count/totalBytes/listFailure/dir"
4349
4382
  );
4350
4383
  }
4351
- async function handleRecipeValidate(req, res) {
4384
+ async function handleRecipeValidate(req, res, bodyReadTimeoutMs = DEFAULT_BODY_READ_TIMEOUT_MS) {
4352
4385
  try {
4353
- await readBody(req, MAX_REQUEST_BODY_BYTES);
4386
+ await readBody(req, MAX_REQUEST_BODY_BYTES, bodyReadTimeoutMs);
4354
4387
  } catch (error) {
4355
4388
  if (error instanceof HTTPPayloadTooLargeError) {
4356
4389
  fail(
@@ -4362,6 +4395,16 @@ async function handleRecipeValidate(req, res) {
4362
4395
  );
4363
4396
  return;
4364
4397
  }
4398
+ if (error instanceof HTTPBodyUnreadError) {
4399
+ fail(
4400
+ res,
4401
+ 408,
4402
+ HTTP_BODY_UNREAD,
4403
+ `request body never completed: ${error.message}`,
4404
+ `send the whole body in one request and retry; this gateway waits ${bodyReadTimeoutMs}ms for one body before it answers this way`
4405
+ );
4406
+ return;
4407
+ }
4365
4408
  fail(res, 400, HTTP_BAD_REQUEST, "could not read the request body", "resend the request");
4366
4409
  return;
4367
4410
  }
@@ -4394,7 +4437,7 @@ function tokenMatches(expected, received) {
4394
4437
  return timingSafeEqual(expectedDigest, receivedDigest);
4395
4438
  }
4396
4439
  function sha256(data) {
4397
- return createHash2("sha256").update(data).digest();
4440
+ return createHash3("sha256").update(data).digest();
4398
4441
  }
4399
4442
  function parseOperationId(value) {
4400
4443
  if (typeof value !== "string") {
@@ -4468,8 +4511,11 @@ function makeHardFailureNote(failure, format) {
4468
4511
  const sentence = `incomplete: ${failure.operationId} failed with ${failure.line} \u2014 the fan-out stopped there, so the packs above are what this request read, not the whole list`;
4469
4512
  return format === "html" ? `<p class="gp-incomplete">${escapeHTML(sentence)}</p>` : `> ${sentence}`;
4470
4513
  }
4471
- function ok(res, result, attribution) {
4472
- writeJson(res, 200, attribution === void 0 ? { ok: true, result } : { ok: true, result, attribution });
4514
+ function ok(res, result, attribution, dimension) {
4515
+ const body = { ok: true, result };
4516
+ if (attribution !== void 0) body.attribution = attribution;
4517
+ if (dimension !== void 0) body.dimension = dimensionAttributionLine(dimension);
4518
+ writeJson(res, 200, body);
4473
4519
  }
4474
4520
  function fail(res, status, code, message, remedy) {
4475
4521
  writeJson(res, status, { ok: false, error: { code, message: forHttpCaller(message), remedy: forHttpCaller(remedy) } });
@@ -4601,22 +4647,50 @@ var HTTPPayloadTooLargeError = class extends Error {
4601
4647
  this.name = "HTTPPayloadTooLargeError";
4602
4648
  }
4603
4649
  };
4604
- function readBody(req, limit) {
4650
+ var HTTPBodyUnreadError = class extends Error {
4651
+ constructor(reason, timeoutMs) {
4652
+ super(`request body did not arrive (${reason}; ceiling ${timeoutMs}ms)`);
4653
+ this.name = "HTTPBodyUnreadError";
4654
+ }
4655
+ };
4656
+ var DEFAULT_BODY_READ_TIMEOUT_MS = 15e3;
4657
+ function readBody(req, limit, timeoutMs = DEFAULT_BODY_READ_TIMEOUT_MS) {
4605
4658
  return new Promise((resolve, reject) => {
4606
4659
  let size = 0;
4607
4660
  const chunks = [];
4661
+ let settled = false;
4662
+ const finish = (action) => {
4663
+ if (settled) return;
4664
+ settled = true;
4665
+ clearTimeout(timer);
4666
+ action();
4667
+ };
4668
+ const timer = setTimeout(() => {
4669
+ finish(() => {
4670
+ chunks.length = 0;
4671
+ req.pause();
4672
+ reject(new HTTPBodyUnreadError("no complete body within the read ceiling", timeoutMs));
4673
+ });
4674
+ }, timeoutMs);
4675
+ timer.unref?.();
4608
4676
  req.on("data", (chunk) => {
4609
4677
  size += chunk.length;
4610
4678
  if (size > limit) {
4611
- chunks.length = 0;
4612
- req.pause();
4613
- reject(new HTTPPayloadTooLargeError(limit));
4679
+ finish(() => {
4680
+ chunks.length = 0;
4681
+ req.pause();
4682
+ reject(new HTTPPayloadTooLargeError(limit));
4683
+ });
4614
4684
  return;
4615
4685
  }
4616
4686
  chunks.push(chunk);
4617
4687
  });
4618
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
4619
- req.on("error", (error) => reject(error));
4688
+ req.on("end", () => finish(() => resolve(Buffer.concat(chunks).toString("utf8"))));
4689
+ req.on("close", () => finish(() => {
4690
+ chunks.length = 0;
4691
+ reject(new HTTPBodyUnreadError("the connection closed before the body ended", timeoutMs));
4692
+ }));
4693
+ req.on("error", (error) => finish(() => reject(error)));
4620
4694
  });
4621
4695
  }
4622
4696
 
@@ -109,6 +109,16 @@ export interface HttpGatewayOptions {
109
109
  * stderr* — a promise that has to have a writer behind it (R8-高3).
110
110
  */
111
111
  report?: (note: string) => void;
112
+ /**
113
+ * Ceiling on how long one request body may take to arrive (finding B1,
114
+ * 2026-10-27). `readBody` had a byte cap and no *time* cap, and the server set
115
+ * no `requestTimeout`, so a caller that sent headers and then stalled the body
116
+ * held a handler open until Node's own default fired and destroyed the socket —
117
+ * which answers the caller with an unstructured `ECONNRESET`, the exact shape
118
+ * this file already refuses for oversized bodies. Tests may shorten it; it is
119
+ * never allowed to be absent.
120
+ */
121
+ bodyReadTimeoutMs?: number;
112
122
  }
113
123
  export interface HttpGateway {
114
124
  /**
@@ -152,4 +162,6 @@ export declare function isLoopbackHost(value: string): boolean;
152
162
  export declare function assertLoopbackHost(host: string): string;
153
163
  /** Refuse an empty token, and one too short or too uniform to be a credential. */
154
164
  export declare function assertTokenPolicy(token: string): void;
165
+ /** How long one request body may take, when the caller did not choose (see {@link HttpGatewayOptions.bodyReadTimeoutMs}). */
166
+ export declare const DEFAULT_BODY_READ_TIMEOUT_MS = 15000;
155
167
  //# sourceMappingURL=http-gateway.d.ts.map
@@ -1,6 +1,6 @@
1
1
  // src/http-gateway.ts
2
2
  import http from "node:http";
3
- import { createHash as createHash2, timingSafeEqual } from "node:crypto";
3
+ import { createHash as createHash3, timingSafeEqual } from "node:crypto";
4
4
  import { isIP } from "node:net";
5
5
  import { URL } from "node:url";
6
6
 
@@ -8,6 +8,7 @@ import { URL } from "node:url";
8
8
  import net from "node:net";
9
9
  import path from "node:path";
10
10
  import fs from "node:fs";
11
+ import { createHash } from "node:crypto";
11
12
  import { fileURLToPath } from "node:url";
12
13
 
13
14
  // src/io.ts
@@ -752,9 +753,9 @@ var EngineJsonRpcClient = class {
752
753
  );
753
754
  return;
754
755
  }
755
- const bodyBytes = Buffer.byteLength(jsonOrString(frame.result), "utf8");
756
+ const bodyJson = jsonOrString(frame.result);
756
757
  this.report(
757
- `late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: the engine did answer, with a ${bodyBytes}-byte body that is raw engine content (an accessibility tree whose title/value fields are on-screen data), so its text is not written to this log${byOrder}`
758
+ `late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: the engine did answer, ${lateReplyBodyNote(bodyJson, operationIdOf(frame.result))}${byOrder}`
758
759
  );
759
760
  if (frame.result === void 0) {
760
761
  return;
@@ -909,6 +910,13 @@ function attribute(error, entry, inferred = true) {
909
910
  function truncate(text) {
910
911
  return text.length <= LOG_BODY_CHARS ? text : `${text.slice(0, LOG_BODY_CHARS)}\u2026 [+${text.length - LOG_BODY_CHARS} chars not logged]`;
911
912
  }
913
+ var ARCHIVE_OPERATION_ID = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
914
+ function lateReplyBodyNote(serializedBody, operationId) {
915
+ const bytes = Buffer.byteLength(serializedBody, "utf8");
916
+ const digest = createHash("sha256").update(serializedBody, "utf8").digest("hex").slice(0, 12);
917
+ const named2 = typeof operationId === "string" && ARCHIVE_OPERATION_ID.test(operationId) ? `, for ${operationId}` : "";
918
+ return `with a ${bytes}-byte body (sha256 ${digest})${named2} that is on-screen accessibility content \u2014 element titles and values, sometimes a document's text \u2014 so its text is not written to this log; the reply itself is in the daemon's evidence archive under the state root, read it back with gp_last_evidence {operationId} or gp_export_evidence {operationId}`;
919
+ }
912
920
  function nonBlankOr(value, fallback) {
913
921
  if (value === void 0 || value === null) {
914
922
  return fallback;
@@ -919,6 +927,9 @@ function nonBlankOr(value, fallback) {
919
927
  function jsonOrString(value) {
920
928
  return String(JSON.stringify(value));
921
929
  }
930
+ function operationIdOf(result) {
931
+ return typeof result === "object" && result !== null ? result.operationId : void 0;
932
+ }
922
933
  var ReconnectingSocketIo = class {
923
934
  constructor(socketPath, open) {
924
935
  this.socketPath = socketPath;
@@ -1086,6 +1097,9 @@ function engineBody(params) {
1086
1097
  const { dimension: _dimension, ...rest } = params;
1087
1098
  return rest;
1088
1099
  }
1100
+ function dimensionAttributionLine(dimension) {
1101
+ return `dimension: ${dimension} | declared by this call; this shell records no decision, so the ledger gains an attribution only where a decision is written for this operation`;
1102
+ }
1089
1103
 
1090
1104
  // src/evidence-report.ts
1091
1105
  var PLACEHOLDER = "\u2014";
@@ -1953,14 +1967,21 @@ function attributeCapability(engineMethod, query = {}) {
1953
1967
  };
1954
1968
  } catch (error) {
1955
1969
  if (error instanceof KernelProviderError) {
1956
- return { status: "refused", error: { code: error.code, message: error.message, remedy: error.remedy } };
1970
+ return {
1971
+ status: "refused",
1972
+ error: {
1973
+ code: GP_E_INTERNAL,
1974
+ message: `provider registration refused (${error.code}): ${error.message}`,
1975
+ remedy: error.remedy
1976
+ }
1977
+ };
1957
1978
  }
1958
1979
  if (error instanceof KernelSchemaError) {
1959
1980
  return {
1960
1981
  status: "refused",
1961
1982
  error: {
1962
- code: error.code,
1963
- message: `${error.message} \u2014 the kernel refused a registration this product writes itself, built with the version from ${registrationVersionSource(measured, usable(own))}`,
1983
+ code: GP_E_INTERNAL,
1984
+ message: `the kernel refused a registration this product writes itself (${error.code}): ${error.message} \u2014 built with the version from ${registrationVersionSource(measured, usable(own))}`,
1964
1985
  remedy: "fix shellProviderDrafts in mcp-shell/src/providers.ts; it is the contract both harnesses read, so it cannot be relaxed at a call site \u2014 run node scripts/check-kernel-corpus.mjs and compare against kernel/fixtures/provider-registration.ok-01.json"
1965
1986
  }
1966
1987
  };
@@ -2011,7 +2032,7 @@ var TRAIL_PROVENANCE_NOTES = {
2011
2032
  import fs3 from "node:fs";
2012
2033
  import os from "node:os";
2013
2034
  import path3 from "node:path";
2014
- import { createHash, randomBytes } from "node:crypto";
2035
+ import { createHash as createHash2, randomBytes } from "node:crypto";
2015
2036
  import { z as z9 } from "zod";
2016
2037
  var CROCKFORD_BODY_RE = "[0-9A-HJKMNP-TV-Z]{26}";
2017
2038
  var PROJECT_ID_RE = new RegExp(`^prj_${CROCKFORD_BODY_RE}$`);
@@ -2315,7 +2336,7 @@ function registryDigest(filePath) {
2315
2336
  return registryBytesDigest(bytes);
2316
2337
  }
2317
2338
  function registryBytesDigest(bytes) {
2318
- return createHash("sha256").update(bytes).digest("hex");
2339
+ return createHash2("sha256").update(bytes).digest("hex");
2319
2340
  }
2320
2341
  function registryDriftReason(filePath, openedAs, now) {
2321
2342
  if (now === openedAs) {
@@ -3794,6 +3815,7 @@ var HTTP_NOT_FOUND = "GP_HTTP_NOT_FOUND";
3794
3815
  var HTTP_BAD_REQUEST = "GP_HTTP_BAD_REQUEST";
3795
3816
  var HTTP_PAYLOAD_TOO_LARGE = "GP_HTTP_PAYLOAD_TOO_LARGE";
3796
3817
  var HTTP_NOT_PROVIDED = "GP_HTTP_NOT_PROVIDED";
3818
+ var HTTP_BODY_UNREAD = "GP_HTTP_BODY_UNREAD";
3797
3819
  var FORWARDABLE_TOOLS = new Map(
3798
3820
  TOOL_SPECS.filter((spec) => spec.execute === void 0).map((spec) => [spec.engineMethod, spec])
3799
3821
  );
@@ -3841,11 +3863,12 @@ function createHttpGateway(options) {
3841
3863
  const client = options.connectController !== void 0 ? options.connectController(options.socketPath) : unixSocketEngineClient(options.socketPath, { surface: "http" });
3842
3864
  client.onEngineNote?.((note) => report(`engine: ${note}`));
3843
3865
  client.onLateReply?.(({ method, result }) => {
3844
- report(`late engine reply to '${method}' (reply body below, raw engine content and not redacted): ${truncate(stringifyReply(result))}`);
3866
+ report(`late engine reply to '${method}': the engine did answer, ${lateReplyBodyNote(stringifyReply(result), result?.operationId)}`);
3845
3867
  });
3846
3868
  const expectedToken = Buffer.from(options.token, "utf8");
3869
+ const bodyReadTimeoutMs = options.bodyReadTimeoutMs ?? DEFAULT_BODY_READ_TIMEOUT_MS;
3847
3870
  const server = http.createServer((req, res) => {
3848
- handleHttp(req, res, client, expectedToken).catch((error) => {
3871
+ handleHttp(req, res, client, expectedToken, bodyReadTimeoutMs).catch((error) => {
3849
3872
  fail(res, 500, GP_E_INTERNAL, `internal gateway error: ${String(error)}`, gatewayFaultAdvice());
3850
3873
  });
3851
3874
  });
@@ -3925,7 +3948,7 @@ function stringifyReply(result) {
3925
3948
  return String(result);
3926
3949
  }
3927
3950
  }
3928
- async function handleHttp(req, res, client, expectedToken) {
3951
+ async function handleHttp(req, res, client, expectedToken, bodyReadTimeoutMs = DEFAULT_BODY_READ_TIMEOUT_MS) {
3929
3952
  if (!authorized(req, expectedToken)) {
3930
3953
  fail(
3931
3954
  res,
@@ -3942,7 +3965,7 @@ async function handleHttp(req, res, client, expectedToken) {
3942
3965
  return;
3943
3966
  }
3944
3967
  const segments = url.pathname.split("/").filter((part) => part !== "");
3945
- await dispatch(req, res, url, segments, client);
3968
+ await dispatch(req, res, url, segments, client, bodyReadTimeoutMs);
3946
3969
  }
3947
3970
  function parseRequestUrl(raw) {
3948
3971
  if (typeof raw !== "string" || raw === "") {
@@ -3954,7 +3977,7 @@ function parseRequestUrl(raw) {
3954
3977
  return null;
3955
3978
  }
3956
3979
  }
3957
- async function dispatch(req, res, url, segments, client) {
3980
+ async function dispatch(req, res, url, segments, client, bodyReadTimeoutMs = DEFAULT_BODY_READ_TIMEOUT_MS) {
3958
3981
  const [v1, resource, id] = segments;
3959
3982
  if (v1 !== "v1") {
3960
3983
  NotFound(res, url.pathname);
@@ -3979,11 +4002,11 @@ async function dispatch(req, res, url, segments, client) {
3979
4002
  }
3980
4003
  } else if (req.method === "POST") {
3981
4004
  if (resource === "tools" && segments.length === 3 && id !== void 0) {
3982
- await handleToolForward(id, req, client, res);
4005
+ await handleToolForward(id, req, client, res, bodyReadTimeoutMs);
3983
4006
  return;
3984
4007
  }
3985
4008
  if (resource === "recipes" && id === "validate" && segments.length === 3) {
3986
- await handleRecipeValidate(req, res);
4009
+ await handleRecipeValidate(req, res, bodyReadTimeoutMs);
3987
4010
  return;
3988
4011
  }
3989
4012
  }
@@ -4200,7 +4223,7 @@ async function handleEvidenceAggregate(url, client, res) {
4200
4223
  report: sections2.join("\n")
4201
4224
  });
4202
4225
  }
4203
- async function handleToolForward(name, req, client, res) {
4226
+ async function handleToolForward(name, req, client, res, bodyReadTimeoutMs = DEFAULT_BODY_READ_TIMEOUT_MS) {
4204
4227
  const spec = FORWARDABLE_TOOLS.get(name);
4205
4228
  if (spec === void 0) {
4206
4229
  fail(
@@ -4214,7 +4237,7 @@ async function handleToolForward(name, req, client, res) {
4214
4237
  }
4215
4238
  let body;
4216
4239
  try {
4217
- const text = await readBody(req, MAX_REQUEST_BODY_BYTES);
4240
+ const text = await readBody(req, MAX_REQUEST_BODY_BYTES, bodyReadTimeoutMs);
4218
4241
  body = JSON.parse(text);
4219
4242
  } catch (error) {
4220
4243
  if (error instanceof HTTPPayloadTooLargeError) {
@@ -4227,6 +4250,16 @@ async function handleToolForward(name, req, client, res) {
4227
4250
  );
4228
4251
  return;
4229
4252
  }
4253
+ if (error instanceof HTTPBodyUnreadError) {
4254
+ fail(
4255
+ res,
4256
+ 408,
4257
+ HTTP_BODY_UNREAD,
4258
+ `request body never completed: ${error.message}`,
4259
+ `send the whole body in one request (Content-Length must match what you write) and retry; this gateway waits ${bodyReadTimeoutMs}ms for one body before it answers this way`
4260
+ );
4261
+ return;
4262
+ }
4230
4263
  fail(
4231
4264
  res,
4232
4265
  400,
@@ -4280,7 +4313,7 @@ async function handleToolForward(name, req, client, res) {
4280
4313
  return;
4281
4314
  }
4282
4315
  }
4283
- ok(res, result, attribution);
4316
+ ok(res, result, attribution, declared.dimension);
4284
4317
  } catch (error) {
4285
4318
  await failEngine(res, error, spec, engineBody(checked.value));
4286
4319
  }
@@ -4294,9 +4327,9 @@ function handleEvidenceStats(res) {
4294
4327
  "run `glasspaned --evidence-stats` against the same --state-dir this socket serves to get count/totalBytes/listFailure/dir"
4295
4328
  );
4296
4329
  }
4297
- async function handleRecipeValidate(req, res) {
4330
+ async function handleRecipeValidate(req, res, bodyReadTimeoutMs = DEFAULT_BODY_READ_TIMEOUT_MS) {
4298
4331
  try {
4299
- await readBody(req, MAX_REQUEST_BODY_BYTES);
4332
+ await readBody(req, MAX_REQUEST_BODY_BYTES, bodyReadTimeoutMs);
4300
4333
  } catch (error) {
4301
4334
  if (error instanceof HTTPPayloadTooLargeError) {
4302
4335
  fail(
@@ -4308,6 +4341,16 @@ async function handleRecipeValidate(req, res) {
4308
4341
  );
4309
4342
  return;
4310
4343
  }
4344
+ if (error instanceof HTTPBodyUnreadError) {
4345
+ fail(
4346
+ res,
4347
+ 408,
4348
+ HTTP_BODY_UNREAD,
4349
+ `request body never completed: ${error.message}`,
4350
+ `send the whole body in one request and retry; this gateway waits ${bodyReadTimeoutMs}ms for one body before it answers this way`
4351
+ );
4352
+ return;
4353
+ }
4311
4354
  fail(res, 400, HTTP_BAD_REQUEST, "could not read the request body", "resend the request");
4312
4355
  return;
4313
4356
  }
@@ -4340,7 +4383,7 @@ function tokenMatches(expected, received) {
4340
4383
  return timingSafeEqual(expectedDigest, receivedDigest);
4341
4384
  }
4342
4385
  function sha256(data) {
4343
- return createHash2("sha256").update(data).digest();
4386
+ return createHash3("sha256").update(data).digest();
4344
4387
  }
4345
4388
  function parseOperationId(value) {
4346
4389
  if (typeof value !== "string") {
@@ -4414,8 +4457,11 @@ function makeHardFailureNote(failure, format) {
4414
4457
  const sentence = `incomplete: ${failure.operationId} failed with ${failure.line} \u2014 the fan-out stopped there, so the packs above are what this request read, not the whole list`;
4415
4458
  return format === "html" ? `<p class="gp-incomplete">${escapeHTML(sentence)}</p>` : `> ${sentence}`;
4416
4459
  }
4417
- function ok(res, result, attribution) {
4418
- writeJson(res, 200, attribution === void 0 ? { ok: true, result } : { ok: true, result, attribution });
4460
+ function ok(res, result, attribution, dimension) {
4461
+ const body = { ok: true, result };
4462
+ if (attribution !== void 0) body.attribution = attribution;
4463
+ if (dimension !== void 0) body.dimension = dimensionAttributionLine(dimension);
4464
+ writeJson(res, 200, body);
4419
4465
  }
4420
4466
  function fail(res, status, code, message, remedy) {
4421
4467
  writeJson(res, status, { ok: false, error: { code, message: forHttpCaller(message), remedy: forHttpCaller(remedy) } });
@@ -4547,25 +4593,54 @@ var HTTPPayloadTooLargeError = class extends Error {
4547
4593
  this.name = "HTTPPayloadTooLargeError";
4548
4594
  }
4549
4595
  };
4550
- function readBody(req, limit) {
4596
+ var HTTPBodyUnreadError = class extends Error {
4597
+ constructor(reason, timeoutMs) {
4598
+ super(`request body did not arrive (${reason}; ceiling ${timeoutMs}ms)`);
4599
+ this.name = "HTTPBodyUnreadError";
4600
+ }
4601
+ };
4602
+ var DEFAULT_BODY_READ_TIMEOUT_MS = 15e3;
4603
+ function readBody(req, limit, timeoutMs = DEFAULT_BODY_READ_TIMEOUT_MS) {
4551
4604
  return new Promise((resolve, reject) => {
4552
4605
  let size = 0;
4553
4606
  const chunks = [];
4607
+ let settled = false;
4608
+ const finish = (action) => {
4609
+ if (settled) return;
4610
+ settled = true;
4611
+ clearTimeout(timer);
4612
+ action();
4613
+ };
4614
+ const timer = setTimeout(() => {
4615
+ finish(() => {
4616
+ chunks.length = 0;
4617
+ req.pause();
4618
+ reject(new HTTPBodyUnreadError("no complete body within the read ceiling", timeoutMs));
4619
+ });
4620
+ }, timeoutMs);
4621
+ timer.unref?.();
4554
4622
  req.on("data", (chunk) => {
4555
4623
  size += chunk.length;
4556
4624
  if (size > limit) {
4557
- chunks.length = 0;
4558
- req.pause();
4559
- reject(new HTTPPayloadTooLargeError(limit));
4625
+ finish(() => {
4626
+ chunks.length = 0;
4627
+ req.pause();
4628
+ reject(new HTTPPayloadTooLargeError(limit));
4629
+ });
4560
4630
  return;
4561
4631
  }
4562
4632
  chunks.push(chunk);
4563
4633
  });
4564
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
4565
- req.on("error", (error) => reject(error));
4634
+ req.on("end", () => finish(() => resolve(Buffer.concat(chunks).toString("utf8"))));
4635
+ req.on("close", () => finish(() => {
4636
+ chunks.length = 0;
4637
+ reject(new HTTPBodyUnreadError("the connection closed before the body ended", timeoutMs));
4638
+ }));
4639
+ req.on("error", (error) => finish(() => reject(error)));
4566
4640
  });
4567
4641
  }
4568
4642
  export {
4643
+ DEFAULT_BODY_READ_TIMEOUT_MS,
4569
4644
  DEFAULT_GATEWAY_HOST,
4570
4645
  DEFAULT_GATEWAY_PORT,
4571
4646
  EVIDENCE_FANOUT_BUDGET_MS,
package/dist/index.js CHANGED
@@ -769,14 +769,21 @@ function attributeCapability(engineMethod, query = {}) {
769
769
  };
770
770
  } catch (error) {
771
771
  if (error instanceof KernelProviderError) {
772
- return { status: "refused", error: { code: error.code, message: error.message, remedy: error.remedy } };
772
+ return {
773
+ status: "refused",
774
+ error: {
775
+ code: GP_E_INTERNAL,
776
+ message: `provider registration refused (${error.code}): ${error.message}`,
777
+ remedy: error.remedy
778
+ }
779
+ };
773
780
  }
774
781
  if (error instanceof KernelSchemaError) {
775
782
  return {
776
783
  status: "refused",
777
784
  error: {
778
- code: error.code,
779
- message: `${error.message} \u2014 the kernel refused a registration this product writes itself, built with the version from ${registrationVersionSource(measured, usable(own))}`,
785
+ code: GP_E_INTERNAL,
786
+ message: `the kernel refused a registration this product writes itself (${error.code}): ${error.message} \u2014 built with the version from ${registrationVersionSource(measured, usable(own))}`,
780
787
  remedy: "fix shellProviderDrafts in mcp-shell/src/providers.ts; it is the contract both harnesses read, so it cannot be relaxed at a call site \u2014 run node scripts/check-kernel-corpus.mjs and compare against kernel/fixtures/provider-registration.ok-01.json"
781
788
  }
782
789
  };
@@ -1449,6 +1456,7 @@ import net from "node:net";
1449
1456
  import os2 from "node:os";
1450
1457
  import path3 from "node:path";
1451
1458
  import fs4 from "node:fs";
1459
+ import { createHash } from "node:crypto";
1452
1460
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1453
1461
  function daemonRestoreCommand() {
1454
1462
  const here = path3.dirname(fileURLToPath3(import.meta.url));
@@ -2053,9 +2061,9 @@ var EngineJsonRpcClient = class {
2053
2061
  );
2054
2062
  return;
2055
2063
  }
2056
- const bodyBytes = Buffer.byteLength(jsonOrString(frame.result), "utf8");
2064
+ const bodyJson = jsonOrString(frame.result);
2057
2065
  this.report(
2058
- `late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: the engine did answer, with a ${bodyBytes}-byte body that is raw engine content (an accessibility tree whose title/value fields are on-screen data), so its text is not written to this log${byOrder}`
2066
+ `late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: the engine did answer, ${lateReplyBodyNote(bodyJson, operationIdOf(frame.result))}${byOrder}`
2059
2067
  );
2060
2068
  if (frame.result === void 0) {
2061
2069
  return;
@@ -2210,6 +2218,13 @@ function attribute(error, entry, inferred = true) {
2210
2218
  function truncate(text) {
2211
2219
  return text.length <= LOG_BODY_CHARS ? text : `${text.slice(0, LOG_BODY_CHARS)}\u2026 [+${text.length - LOG_BODY_CHARS} chars not logged]`;
2212
2220
  }
2221
+ var ARCHIVE_OPERATION_ID = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
2222
+ function lateReplyBodyNote(serializedBody, operationId) {
2223
+ const bytes = Buffer.byteLength(serializedBody, "utf8");
2224
+ const digest = createHash("sha256").update(serializedBody, "utf8").digest("hex").slice(0, 12);
2225
+ const named2 = typeof operationId === "string" && ARCHIVE_OPERATION_ID.test(operationId) ? `, for ${operationId}` : "";
2226
+ return `with a ${bytes}-byte body (sha256 ${digest})${named2} that is on-screen accessibility content \u2014 element titles and values, sometimes a document's text \u2014 so its text is not written to this log; the reply itself is in the daemon's evidence archive under the state root, read it back with gp_last_evidence {operationId} or gp_export_evidence {operationId}`;
2227
+ }
2213
2228
  function nonBlankOr(value, fallback) {
2214
2229
  if (value === void 0 || value === null) {
2215
2230
  return fallback;
@@ -2220,6 +2235,9 @@ function nonBlankOr(value, fallback) {
2220
2235
  function jsonOrString(value) {
2221
2236
  return String(JSON.stringify(value));
2222
2237
  }
2238
+ function operationIdOf(result) {
2239
+ return typeof result === "object" && result !== null ? result.operationId : void 0;
2240
+ }
2223
2241
  var ReconnectingSocketIo = class {
2224
2242
  constructor(socketPath, open) {
2225
2243
  this.socketPath = socketPath;
@@ -2764,7 +2782,7 @@ function exponentialFromDigits(digits, power) {
2764
2782
  import fs5 from "node:fs";
2765
2783
  import os3 from "node:os";
2766
2784
  import path4 from "node:path";
2767
- import { createHash, randomBytes } from "node:crypto";
2785
+ import { createHash as createHash2, randomBytes } from "node:crypto";
2768
2786
  import { z as z9 } from "zod";
2769
2787
  var CROCKFORD_BODY_RE = "[0-9A-HJKMNP-TV-Z]{26}";
2770
2788
  var PROJECT_ID_RE = new RegExp(`^prj_${CROCKFORD_BODY_RE}$`);
@@ -3068,7 +3086,7 @@ function registryDigest(filePath) {
3068
3086
  return registryBytesDigest(bytes);
3069
3087
  }
3070
3088
  function registryBytesDigest(bytes) {
3071
- return createHash("sha256").update(bytes).digest("hex");
3089
+ return createHash2("sha256").update(bytes).digest("hex");
3072
3090
  }
3073
3091
  function registryDriftReason(filePath, openedAs, now) {
3074
3092
  if (now === openedAs) {
@@ -4819,7 +4837,7 @@ var SUPPORTED_PROTOCOL_VERSIONS = [
4819
4837
  "2025-03-26",
4820
4838
  MCP_PROTOCOL_VERSION
4821
4839
  ];
4822
- var SERVER_INFO = { name: "glasspane-mcp", version: "1.22.0" };
4840
+ var SERVER_INFO = { name: "glasspane-mcp", version: "1.24.0" };
4823
4841
  var PARSE_ERROR = -32700;
4824
4842
  var INVALID_REQUEST = -32600;
4825
4843
  var METHOD_NOT_FOUND = -32601;
@@ -764,14 +764,21 @@ function attributeCapability(engineMethod, query = {}) {
764
764
  };
765
765
  } catch (error) {
766
766
  if (error instanceof KernelProviderError) {
767
- return { status: "refused", error: { code: error.code, message: error.message, remedy: error.remedy } };
767
+ return {
768
+ status: "refused",
769
+ error: {
770
+ code: GP_E_INTERNAL,
771
+ message: `provider registration refused (${error.code}): ${error.message}`,
772
+ remedy: error.remedy
773
+ }
774
+ };
768
775
  }
769
776
  if (error instanceof KernelSchemaError) {
770
777
  return {
771
778
  status: "refused",
772
779
  error: {
773
- code: error.code,
774
- message: `${error.message} \u2014 the kernel refused a registration this product writes itself, built with the version from ${registrationVersionSource(measured, usable(own))}`,
780
+ code: GP_E_INTERNAL,
781
+ message: `the kernel refused a registration this product writes itself (${error.code}): ${error.message} \u2014 built with the version from ${registrationVersionSource(measured, usable(own))}`,
775
782
  remedy: "fix shellProviderDrafts in mcp-shell/src/providers.ts; it is the contract both harnesses read, so it cannot be relaxed at a call site \u2014 run node scripts/check-kernel-corpus.mjs and compare against kernel/fixtures/provider-registration.ok-01.json"
776
783
  }
777
784
  };
@@ -1444,6 +1451,7 @@ import net from "node:net";
1444
1451
  import os2 from "node:os";
1445
1452
  import path3 from "node:path";
1446
1453
  import fs4 from "node:fs";
1454
+ import { createHash } from "node:crypto";
1447
1455
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1448
1456
  function daemonRestoreCommand() {
1449
1457
  const here = path3.dirname(fileURLToPath3(import.meta.url));
@@ -2048,9 +2056,9 @@ var EngineJsonRpcClient = class {
2048
2056
  );
2049
2057
  return;
2050
2058
  }
2051
- const bodyBytes = Buffer.byteLength(jsonOrString(frame.result), "utf8");
2059
+ const bodyJson = jsonOrString(frame.result);
2052
2060
  this.report(
2053
- `late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: the engine did answer, with a ${bodyBytes}-byte body that is raw engine content (an accessibility tree whose title/value fields are on-screen data), so its text is not written to this log${byOrder}`
2061
+ `late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: the engine did answer, ${lateReplyBodyNote(bodyJson, operationIdOf(frame.result))}${byOrder}`
2054
2062
  );
2055
2063
  if (frame.result === void 0) {
2056
2064
  return;
@@ -2205,6 +2213,13 @@ function attribute(error, entry, inferred = true) {
2205
2213
  function truncate(text) {
2206
2214
  return text.length <= LOG_BODY_CHARS ? text : `${text.slice(0, LOG_BODY_CHARS)}\u2026 [+${text.length - LOG_BODY_CHARS} chars not logged]`;
2207
2215
  }
2216
+ var ARCHIVE_OPERATION_ID = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
2217
+ function lateReplyBodyNote(serializedBody, operationId) {
2218
+ const bytes = Buffer.byteLength(serializedBody, "utf8");
2219
+ const digest = createHash("sha256").update(serializedBody, "utf8").digest("hex").slice(0, 12);
2220
+ const named2 = typeof operationId === "string" && ARCHIVE_OPERATION_ID.test(operationId) ? `, for ${operationId}` : "";
2221
+ return `with a ${bytes}-byte body (sha256 ${digest})${named2} that is on-screen accessibility content \u2014 element titles and values, sometimes a document's text \u2014 so its text is not written to this log; the reply itself is in the daemon's evidence archive under the state root, read it back with gp_last_evidence {operationId} or gp_export_evidence {operationId}`;
2222
+ }
2208
2223
  function nonBlankOr(value, fallback) {
2209
2224
  if (value === void 0 || value === null) {
2210
2225
  return fallback;
@@ -2215,6 +2230,9 @@ function nonBlankOr(value, fallback) {
2215
2230
  function jsonOrString(value) {
2216
2231
  return String(JSON.stringify(value));
2217
2232
  }
2233
+ function operationIdOf(result) {
2234
+ return typeof result === "object" && result !== null ? result.operationId : void 0;
2235
+ }
2218
2236
  var ReconnectingSocketIo = class {
2219
2237
  constructor(socketPath, open) {
2220
2238
  this.socketPath = socketPath;
@@ -2759,7 +2777,7 @@ function exponentialFromDigits(digits, power) {
2759
2777
  import fs5 from "node:fs";
2760
2778
  import os3 from "node:os";
2761
2779
  import path4 from "node:path";
2762
- import { createHash, randomBytes } from "node:crypto";
2780
+ import { createHash as createHash2, randomBytes } from "node:crypto";
2763
2781
  import { z as z9 } from "zod";
2764
2782
  var CROCKFORD_BODY_RE = "[0-9A-HJKMNP-TV-Z]{26}";
2765
2783
  var PROJECT_ID_RE = new RegExp(`^prj_${CROCKFORD_BODY_RE}$`);
@@ -3063,7 +3081,7 @@ function registryDigest(filePath) {
3063
3081
  return registryBytesDigest(bytes);
3064
3082
  }
3065
3083
  function registryBytesDigest(bytes) {
3066
- return createHash("sha256").update(bytes).digest("hex");
3084
+ return createHash2("sha256").update(bytes).digest("hex");
3067
3085
  }
3068
3086
  function registryDriftReason(filePath, openedAs, now) {
3069
3087
  if (now === openedAs) {
@@ -4814,7 +4832,7 @@ var SUPPORTED_PROTOCOL_VERSIONS = [
4814
4832
  "2025-03-26",
4815
4833
  MCP_PROTOCOL_VERSION
4816
4834
  ];
4817
- var SERVER_INFO = { name: "glasspane-mcp", version: "1.22.0" };
4835
+ var SERVER_INFO = { name: "glasspane-mcp", version: "1.24.0" };
4818
4836
  var PARSE_ERROR = -32700;
4819
4837
  var INVALID_REQUEST = -32600;
4820
4838
  var METHOD_NOT_FOUND = -32601;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glasspane-mcp",
3
- "version": "1.22.0",
3
+ "version": "1.24.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",