glasspane-mcp 1.3.1 → 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.3.1";
29
+ readonly version: "1.5.0";
30
30
  };
31
31
  export declare const PARSE_ERROR = -32700;
32
32
  export declare const INVALID_REQUEST = -32600;
@@ -139,6 +139,28 @@ export declare const RESTORE_BASE_DEADLINE_MS = 30000;
139
139
  export declare function isReplayUnsafeMethod(method: string): boolean;
140
140
  /** The non-replayable methods, for gates that must see the set change on purpose. */
141
141
  export declare const REPLAY_UNSAFE_METHOD_NAMES: readonly string[];
142
+ /**
143
+ * Methods whose re-issue writes daemon state the *caller* depends on, even though
144
+ * replaying them is not unsafe in the sense {@link REPLAY_UNSAFE_REASONS} means
145
+ * (no operation of the user's is performed a second time). They are the other half
146
+ * of the answer to "may this be narrowed by sending it again?", and they are kept
147
+ * apart from the set above because `test/probe-liveness.test.mjs` pins that set's
148
+ * membership as the no-replay judgement.
149
+ *
150
+ * `snapshot` is here because of a ring, not of a click: `storeSnapshot`
151
+ * (`engine/Sources/GlassPaneEngine/EngineCore.swift:2070`) appends the new
152
+ * baseline and then trims oldest-first to `EngineCore.snapshotHistoryLimit` (the
153
+ * same file, line 25 — 8 entries). A `gp_snapshot` whose caller timed out may
154
+ * therefore have been carried out already, and every re-send — "with a smaller
155
+ * maxDepth", which is what the tool layer used to say — adds another entry that can
156
+ * evict the baseline the caller's *own earlier* snapshot returned, the one
157
+ * `gp_restore {snapshotId}` needs later. Evicting a caller's evidence to fetch a
158
+ * cheaper copy of it is not narrowing.
159
+ *
160
+ * `attach` writes state too, and is already a replay-unsafe method; `tools.ts`
161
+ * unions the two lists rather than repeating names here.
162
+ */
163
+ export declare const STATE_CHANGING_ON_RETRY_METHOD_NAMES: readonly string[];
142
164
  /** Deadline for one request; `restore` scales with its replayed step count. */
143
165
  export declare function engineDeadlineMs(method: string, params?: Record<string, unknown>): number;
144
166
  /** When the caller gets its answer for `method`: its deadline, capped. */
@@ -366,6 +388,12 @@ export declare class EngineJsonRpcClient {
366
388
  * Oldest request this client still tracks, settled ones included: that is the
367
389
  * request the daemon's arrival-ordered answer belongs to. `Map` iteration is
368
390
  * insertion order, which is the order the requests went out.
391
+ *
392
+ * An entry whose id-less answer has already been taken (`arrivalOrderConsumed`)
393
+ * is passed over for *this* purpose while staying in the map for the echoed-id
394
+ * lookup: the guess has been spent once and may not be spent again on the next
395
+ * id-less frame, and the request it was spent on is still the request whose real
396
+ * reply can arrive with its id (finding 中-4).
369
397
  */
370
398
  private oldestTracked;
371
399
  /**
@@ -219,6 +219,7 @@ function isReplayUnsafeMethod(method) {
219
219
  return Object.prototype.hasOwnProperty.call(REPLAY_UNSAFE_REASONS, method);
220
220
  }
221
221
  var REPLAY_UNSAFE_METHOD_NAMES = Object.keys(REPLAY_UNSAFE_REASONS);
222
+ var STATE_CHANGING_ON_RETRY_METHOD_NAMES = ["snapshot"];
222
223
  function engineDeadlineMs(method, params) {
223
224
  if (method === "restore") {
224
225
  const steps = params?.steps;
@@ -275,6 +276,10 @@ var NARROWING_AUTHORITY = {
275
276
  mcp: "the gp_* tool that took this call publishes its own parameters, and their floors, in the schema shown beside that tool \u2014 read it there, not here",
276
277
  http: "this gateway publishes no parameter listing of its own, so the floors a body was accepted against are in the request contract this call was copied from"
277
278
  };
279
+ function clientClosingRemedy(surface) {
280
+ const readRoute = surface === "mcp" ? "gp_recent_reports lists the operations this session recorded and gp_last_evidence {operationId} returns one in full" : "this gateway keeps no operation trail of its own, so the stderr line this reply is written to is the record";
281
+ return `this shell's own engine client is ending, which measures nothing about the daemon: nothing here authorises touching the daemon's lifecycle, and no restart is authorised by this answer. What is unknown stays unknown: whether the daemon finished this request cannot be told from a client that stopped asking. The reply may still land, under the bound this shell already states \u2014 ${lateReplyRoute(surface)} \u2014 and this shutdown is one of the endings that bound names: the server log line written at this moment lists the requests this client stopped tracking. Read what happened rather than performing it a second time: ${readRoute}. Ask through a server process that still has a live engine client.`;
282
+ }
278
283
  function reIssueGuidance(surface) {
279
284
  return `wait for the outstanding reply rather than sending this request again: the daemon answers one request at a time, so a second copy of it now only queues behind the one still running and answers nothing sooner, and if the first did take effect the copy performs it a second time. What is worth narrowing here, and whether it can still be narrowed at the values this request already sent, is a fact about a schema, and this layer holds no schema for any request: ${NARROWING_AUTHORITY[surface]}. Re-send only after reading that, and only if the request is one whose repetition changes nothing`;
280
285
  }
@@ -430,8 +435,8 @@ var EngineJsonRpcClient = class {
430
435
  this.state.closed = true;
431
436
  this.teardown(new EngineCallError(
432
437
  GP_E_ENGINE_UNREACHABLE,
433
- "engine client closed",
434
- daemonUnreachableRemedy()
438
+ "engine client closed: this shell's own client is ending, so whether the daemon finished this request is not known here and this client can no longer attribute the reply that may still land",
439
+ clientClosingRemedy(this.surface)
435
440
  ));
436
441
  this.io.close();
437
442
  }
@@ -480,7 +485,11 @@ var EngineJsonRpcClient = class {
480
485
  if (typeof id === "number") {
481
486
  const entry = this.pending.get(id);
482
487
  if (!entry) {
483
- this.report(`engine sent a frame for request id ${id}, which this client never issued (or already gave up on): ${truncate(line)}`);
488
+ const tracked = [...this.pending.values()];
489
+ const held = tracked.length === 0 ? "nothing is tracked now" : `this client still tracks ${tracked.length} request(s): ` + tracked.map((kept) => "'" + kept.method + "' id " + kept.id + (kept.settled ? " (caller already answered)" : "")).join(", ");
490
+ this.report(
491
+ `engine sent a frame for request id ${id}, which this client never issued (or already gave up on); ` + held + ": " + truncate(line)
492
+ );
484
493
  return;
485
494
  }
486
495
  this.pending.delete(id);
@@ -495,8 +504,9 @@ var EngineJsonRpcClient = class {
495
504
  const target = this.oldestTracked();
496
505
  if (target) {
497
506
  if (target.settled) {
498
- this.pending.delete(target.id);
499
507
  this.reportLateReply(target, frame, true);
508
+ target.arrivalOrderConsumed = true;
509
+ this.retainForLateReply();
500
510
  return;
501
511
  }
502
512
  if (frame.error) {
@@ -557,9 +567,20 @@ var EngineJsonRpcClient = class {
557
567
  * Oldest request this client still tracks, settled ones included: that is the
558
568
  * request the daemon's arrival-ordered answer belongs to. `Map` iteration is
559
569
  * insertion order, which is the order the requests went out.
570
+ *
571
+ * An entry whose id-less answer has already been taken (`arrivalOrderConsumed`)
572
+ * is passed over for *this* purpose while staying in the map for the echoed-id
573
+ * lookup: the guess has been spent once and may not be spent again on the next
574
+ * id-less frame, and the request it was spent on is still the request whose real
575
+ * reply can arrive with its id (finding 中-4).
560
576
  */
561
577
  oldestTracked() {
562
- return this.pending.values().next().value;
578
+ for (const entry of this.pending.values()) {
579
+ if (!entry.arrivalOrderConsumed) {
580
+ return entry;
581
+ }
582
+ }
583
+ return void 0;
563
584
  }
564
585
  /**
565
586
  * Route the reply that lands after its caller was already answered. `inferred`
@@ -1041,8 +1062,8 @@ var LEGACY_EVIDENCE_SCHEMA_VERSION = "glasspane.evidence/0.1-draft";
1041
1062
  var AttributionLevelSchema = z.enum(["soft", "strong", "weak"]);
1042
1063
  var CircuitBreakerLevelSchema = z.number().int().min(0).max(3);
1043
1064
  var SelectorSchema = z.strictObject({
1044
- role: z.string().max(SELECTOR_MAX_LENGTH),
1045
- title: z.string().max(SELECTOR_MAX_LENGTH).optional(),
1065
+ role: z.string().min(1).max(SELECTOR_MAX_LENGTH),
1066
+ title: z.string().min(1).max(SELECTOR_MAX_LENGTH).optional(),
1046
1067
  identifier: z.string().max(SELECTOR_MAX_LENGTH).optional()
1047
1068
  });
1048
1069
  var ActionSchema = z.enum([
@@ -1279,6 +1300,10 @@ function sortKeys(value) {
1279
1300
  return value;
1280
1301
  }
1281
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
+
1282
1307
  // src/project-registry.ts
1283
1308
  import fs2 from "node:fs";
1284
1309
  import os2 from "node:os";
@@ -1794,14 +1819,9 @@ var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
1794
1819
  var OptionalOperationIdSchema = z5.string().regex(OPERATION_ID_PATTERN2).optional();
1795
1820
  var OptionalProjectIdSchema = z5.string().regex(/^prj_[0-9A-HJKMNP-TV-Z]{26}$/).optional();
1796
1821
  var ExpectSchema = z5.union([z5.string().max(512), z5.boolean()]);
1797
- var ShellSelectorSchema = z5.strictObject({
1798
- role: z5.string().min(1).max(SELECTOR_MAX_LENGTH),
1799
- title: z5.string().min(1).max(SELECTOR_MAX_LENGTH).optional(),
1800
- identifier: z5.string().max(SELECTOR_MAX_LENGTH).optional()
1801
- });
1802
1822
  var SnapshotIdSchema = z5.string().regex(/^snap_[0-9A-HJKMNP-TV-Z]{26}$/);
1803
1823
  var ActStepSchema = z5.strictObject({
1804
- selector: ShellSelectorSchema,
1824
+ selector: SelectorSchema,
1805
1825
  action: ActionSchema
1806
1826
  });
1807
1827
  var RestoreStepsSchema = z5.array(ActStepSchema).min(1).max(64).optional();
@@ -1841,12 +1861,12 @@ var AuditUiArgs = z5.strictObject({
1841
1861
  minHitTargetPt: OptionalHitTargetSchema
1842
1862
  });
1843
1863
  var ActArgs = z5.strictObject({
1844
- selector: ShellSelectorSchema,
1864
+ selector: SelectorSchema,
1845
1865
  action: ActionSchema,
1846
1866
  degrade: z5.boolean().optional()
1847
1867
  });
1848
1868
  var AssertElementArgs = z5.strictObject({
1849
- selector: ShellSelectorSchema,
1869
+ selector: SelectorSchema,
1850
1870
  property: AssertionPropertySchema,
1851
1871
  expected: ExpectSchema
1852
1872
  });
@@ -1890,9 +1910,11 @@ var selectorJsonSchema = {
1890
1910
  type: "object",
1891
1911
  additionalProperties: false,
1892
1912
  properties: {
1893
- // `minLength` here is the advertised half of {@link ShellSelectorSchema};
1913
+ // `minLength` here is the advertised half of the kernel's `SelectorSchema`;
1894
1914
  // `test/tools.test.mjs` compares the two against each other and against the
1895
- // daemon's own rule, so neither side can be tightened or loosened alone.
1915
+ // daemon's own rule, and `test/consumer-consistency.test.mjs` compares both
1916
+ // against the published `kernel/schemas/evidence-pack.schema.json`, so no
1917
+ // side can be tightened or loosened alone.
1896
1918
  role: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH },
1897
1919
  title: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH },
1898
1920
  identifier: { type: "string", maxLength: SELECTOR_MAX_LENGTH }
@@ -2176,6 +2198,25 @@ var TOOL_SPECS = [
2176
2198
  var TOOL_BY_NAME = new Map(
2177
2199
  TOOL_SPECS.map((spec) => [spec.name, spec])
2178
2200
  );
2201
+ var TARGET_SELECTING_KNOBS = {
2202
+ gp_act: {
2203
+ param: "selector",
2204
+ picks: "which control the action is performed on",
2205
+ substitute: "a different control takes the press, and the user's screen changes in a way no one asked for"
2206
+ },
2207
+ gp_assert_element: {
2208
+ param: "selector",
2209
+ picks: "which element the claim is about",
2210
+ substitute: "the answer then describes a different element, while this session's trail still files it as the verdict on the claim you first made"
2211
+ }
2212
+ };
2213
+ function targetSelectingKnob(specName, paramName) {
2214
+ if (!Object.prototype.hasOwnProperty.call(TARGET_SELECTING_KNOBS, specName)) {
2215
+ return null;
2216
+ }
2217
+ const entry = TARGET_SELECTING_KNOBS[specName];
2218
+ return entry !== void 0 && entry.param === paramName ? { picks: entry.picks, substitute: entry.substitute } : null;
2219
+ }
2179
2220
  function narrowingKnobsFor(spec, params) {
2180
2221
  const properties = spec.inputSchema.properties ?? {};
2181
2222
  const knobs = [];
@@ -2190,11 +2231,21 @@ function narrowingKnobsFor(spec, params) {
2190
2231
  } else if (name === "limit") {
2191
2232
  knobs.push({ name, kind: "limit", minimum: schema.minimum, maximum: schema.maximum, sent });
2192
2233
  } else if (name === "selector" || name === "role" || name === "title") {
2193
- knobs.push({ name, kind: "selector", sent: void 0 });
2234
+ const target = targetSelectingKnob(spec.name, name);
2235
+ knobs.push(target === null ? { name, kind: "selector", sent: void 0 } : { name, kind: "target", sent: void 0, picks: target.picks, substitute: target.substitute });
2194
2236
  }
2195
2237
  }
2196
2238
  return knobs;
2197
2239
  }
2240
+ function replyShrinkingKnobs(knobs) {
2241
+ return knobs.filter((knob) => knob.kind !== "target");
2242
+ }
2243
+ function targetKnobs(knobs) {
2244
+ return knobs.filter((knob) => knob.kind === "target");
2245
+ }
2246
+ function describeTargetKnob(knob) {
2247
+ return `'${knob.name}' names ${knob.picks ?? "which element this operation is about"}, so changing it is not a smaller or cheaper request \u2014 it is a different operation, and ${knob.substitute ?? "the answer then describes something else"}`;
2248
+ }
2198
2249
  function atAdvertisedFloor(knob) {
2199
2250
  return knob.minimum !== void 0 && knob.sent !== void 0 && knob.sent <= knob.minimum;
2200
2251
  }
@@ -2208,6 +2259,8 @@ function knobSubject(kind) {
2208
2259
  return "the image";
2209
2260
  case "selector":
2210
2261
  return "the reply";
2262
+ case "target":
2263
+ return "the element the operation is about";
2211
2264
  }
2212
2265
  }
2213
2266
  function describeKnob(knob) {
@@ -2224,19 +2277,27 @@ function describeKnob(knob) {
2224
2277
  return knob.name === "selector" ? "a more specific selector (aim at one element)" : `a tighter ${knob.name} filter`;
2225
2278
  case "scale":
2226
2279
  return `a smaller scale${bounds}${knob.sent !== void 0 ? `, currently ${knob.sent}` : ""}`;
2280
+ case "target":
2281
+ return describeTargetKnob(knob);
2227
2282
  }
2228
2283
  }
2229
2284
  function oversizedReplyAdvice(spec, params) {
2230
2285
  const knobs = narrowingKnobsFor(spec, params);
2231
2286
  const atFloor = knobs.filter(atAdvertisedFloor);
2232
- const shrinkable = knobs.filter((knob) => !atAdvertisedFloor(knob));
2287
+ const target = targetKnobs(knobs);
2288
+ const shrinkable = replyShrinkingKnobs(knobs).filter((knob) => !atAdvertisedFloor(knob));
2233
2289
  const head = `the reply was too large for one frame (${MAX_FRAME_BYTES}-byte limit); `;
2234
- const floorNote = atFloor.length === 0 ? "" : `${atFloor.map(describeKnob).join("; ")}; `;
2290
+ const refused = [...atFloor.map(describeKnob), ...target.map(describeTargetKnob)];
2291
+ const floorNote = refused.length === 0 ? "" : `${refused.join("; ")}; `;
2235
2292
  if (shrinkable.length === 0) {
2236
- return knobs.length > 0 ? head + `${atFloor.map(describeKnob).join("; ")} \u2014 nothing left to narrow on this request: ask a smaller question (one element, one region) rather than retrying it unchanged; the connection is intact and the daemon keeps answering other requests` : head + "this tool advertises no parameter that shrinks a reply, so do not retry it unchanged \u2014 ask a smaller question (one element, one region) instead; the connection is intact and the daemon keeps answering other requests";
2293
+ return knobs.length > 0 ? head + floorNote + "nothing left to narrow on this request: " + (target.length > 0 ? "this request already carries one element and one region, so read the tree with gp_observe and, if the element you meant is another one, make that its own claim rather than treating this answer as a size problem" : "ask a smaller question (one element, one region) rather than retrying it unchanged") + "; the connection is intact and the daemon keeps answering other requests" : head + "this tool advertises no parameter that shrinks a reply, so do not retry it unchanged \u2014 ask a smaller question (one element, one region) instead; the connection is intact and the daemon keeps answering other requests";
2237
2294
  }
2238
2295
  return head + floorNote + `retry with ${shrinkable.map(describeKnob).join(" and ")}; the reply body has to fit one frame, and a retried identical request will be dropped identically; the connection is intact and the daemon keeps answering other requests`;
2239
2296
  }
2297
+ var NOT_NARROWABLE_BY_RETRY = /* @__PURE__ */ new Set([
2298
+ ...REPLAY_UNSAFE_METHOD_NAMES,
2299
+ ...STATE_CHANGING_ON_RETRY_METHOD_NAMES
2300
+ ]);
2240
2301
  async function readTrail(session, turn, limit) {
2241
2302
  await turn.acquire();
2242
2303
  try {
@@ -215,6 +215,7 @@ function isReplayUnsafeMethod(method) {
215
215
  return Object.prototype.hasOwnProperty.call(REPLAY_UNSAFE_REASONS, method);
216
216
  }
217
217
  var REPLAY_UNSAFE_METHOD_NAMES = Object.keys(REPLAY_UNSAFE_REASONS);
218
+ var STATE_CHANGING_ON_RETRY_METHOD_NAMES = ["snapshot"];
218
219
  function engineDeadlineMs(method, params) {
219
220
  if (method === "restore") {
220
221
  const steps = params?.steps;
@@ -271,6 +272,10 @@ var NARROWING_AUTHORITY = {
271
272
  mcp: "the gp_* tool that took this call publishes its own parameters, and their floors, in the schema shown beside that tool \u2014 read it there, not here",
272
273
  http: "this gateway publishes no parameter listing of its own, so the floors a body was accepted against are in the request contract this call was copied from"
273
274
  };
275
+ function clientClosingRemedy(surface) {
276
+ const readRoute = surface === "mcp" ? "gp_recent_reports lists the operations this session recorded and gp_last_evidence {operationId} returns one in full" : "this gateway keeps no operation trail of its own, so the stderr line this reply is written to is the record";
277
+ return `this shell's own engine client is ending, which measures nothing about the daemon: nothing here authorises touching the daemon's lifecycle, and no restart is authorised by this answer. What is unknown stays unknown: whether the daemon finished this request cannot be told from a client that stopped asking. The reply may still land, under the bound this shell already states \u2014 ${lateReplyRoute(surface)} \u2014 and this shutdown is one of the endings that bound names: the server log line written at this moment lists the requests this client stopped tracking. Read what happened rather than performing it a second time: ${readRoute}. Ask through a server process that still has a live engine client.`;
278
+ }
274
279
  function reIssueGuidance(surface) {
275
280
  return `wait for the outstanding reply rather than sending this request again: the daemon answers one request at a time, so a second copy of it now only queues behind the one still running and answers nothing sooner, and if the first did take effect the copy performs it a second time. What is worth narrowing here, and whether it can still be narrowed at the values this request already sent, is a fact about a schema, and this layer holds no schema for any request: ${NARROWING_AUTHORITY[surface]}. Re-send only after reading that, and only if the request is one whose repetition changes nothing`;
276
281
  }
@@ -426,8 +431,8 @@ var EngineJsonRpcClient = class {
426
431
  this.state.closed = true;
427
432
  this.teardown(new EngineCallError(
428
433
  GP_E_ENGINE_UNREACHABLE,
429
- "engine client closed",
430
- daemonUnreachableRemedy()
434
+ "engine client closed: this shell's own client is ending, so whether the daemon finished this request is not known here and this client can no longer attribute the reply that may still land",
435
+ clientClosingRemedy(this.surface)
431
436
  ));
432
437
  this.io.close();
433
438
  }
@@ -476,7 +481,11 @@ var EngineJsonRpcClient = class {
476
481
  if (typeof id === "number") {
477
482
  const entry = this.pending.get(id);
478
483
  if (!entry) {
479
- this.report(`engine sent a frame for request id ${id}, which this client never issued (or already gave up on): ${truncate(line)}`);
484
+ const tracked = [...this.pending.values()];
485
+ const held = tracked.length === 0 ? "nothing is tracked now" : `this client still tracks ${tracked.length} request(s): ` + tracked.map((kept) => "'" + kept.method + "' id " + kept.id + (kept.settled ? " (caller already answered)" : "")).join(", ");
486
+ this.report(
487
+ `engine sent a frame for request id ${id}, which this client never issued (or already gave up on); ` + held + ": " + truncate(line)
488
+ );
480
489
  return;
481
490
  }
482
491
  this.pending.delete(id);
@@ -491,8 +500,9 @@ var EngineJsonRpcClient = class {
491
500
  const target = this.oldestTracked();
492
501
  if (target) {
493
502
  if (target.settled) {
494
- this.pending.delete(target.id);
495
503
  this.reportLateReply(target, frame, true);
504
+ target.arrivalOrderConsumed = true;
505
+ this.retainForLateReply();
496
506
  return;
497
507
  }
498
508
  if (frame.error) {
@@ -553,9 +563,20 @@ var EngineJsonRpcClient = class {
553
563
  * Oldest request this client still tracks, settled ones included: that is the
554
564
  * request the daemon's arrival-ordered answer belongs to. `Map` iteration is
555
565
  * insertion order, which is the order the requests went out.
566
+ *
567
+ * An entry whose id-less answer has already been taken (`arrivalOrderConsumed`)
568
+ * is passed over for *this* purpose while staying in the map for the echoed-id
569
+ * lookup: the guess has been spent once and may not be spent again on the next
570
+ * id-less frame, and the request it was spent on is still the request whose real
571
+ * reply can arrive with its id (finding 中-4).
556
572
  */
557
573
  oldestTracked() {
558
- return this.pending.values().next().value;
574
+ for (const entry of this.pending.values()) {
575
+ if (!entry.arrivalOrderConsumed) {
576
+ return entry;
577
+ }
578
+ }
579
+ return void 0;
559
580
  }
560
581
  /**
561
582
  * Route the reply that lands after its caller was already answered. `inferred`
@@ -1031,8 +1052,8 @@ var LEGACY_EVIDENCE_SCHEMA_VERSION = "glasspane.evidence/0.1-draft";
1031
1052
  var AttributionLevelSchema = z.enum(["soft", "strong", "weak"]);
1032
1053
  var CircuitBreakerLevelSchema = z.number().int().min(0).max(3);
1033
1054
  var SelectorSchema = z.strictObject({
1034
- role: z.string().max(SELECTOR_MAX_LENGTH),
1035
- title: z.string().max(SELECTOR_MAX_LENGTH).optional(),
1055
+ role: z.string().min(1).max(SELECTOR_MAX_LENGTH),
1056
+ title: z.string().min(1).max(SELECTOR_MAX_LENGTH).optional(),
1036
1057
  identifier: z.string().max(SELECTOR_MAX_LENGTH).optional()
1037
1058
  });
1038
1059
  var ActionSchema = z.enum([
@@ -1269,6 +1290,10 @@ function sortKeys(value) {
1269
1290
  return value;
1270
1291
  }
1271
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
+
1272
1297
  // src/project-registry.ts
1273
1298
  import fs2 from "node:fs";
1274
1299
  import os from "node:os";
@@ -1784,14 +1809,9 @@ var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
1784
1809
  var OptionalOperationIdSchema = z5.string().regex(OPERATION_ID_PATTERN2).optional();
1785
1810
  var OptionalProjectIdSchema = z5.string().regex(/^prj_[0-9A-HJKMNP-TV-Z]{26}$/).optional();
1786
1811
  var ExpectSchema = z5.union([z5.string().max(512), z5.boolean()]);
1787
- var ShellSelectorSchema = z5.strictObject({
1788
- role: z5.string().min(1).max(SELECTOR_MAX_LENGTH),
1789
- title: z5.string().min(1).max(SELECTOR_MAX_LENGTH).optional(),
1790
- identifier: z5.string().max(SELECTOR_MAX_LENGTH).optional()
1791
- });
1792
1812
  var SnapshotIdSchema = z5.string().regex(/^snap_[0-9A-HJKMNP-TV-Z]{26}$/);
1793
1813
  var ActStepSchema = z5.strictObject({
1794
- selector: ShellSelectorSchema,
1814
+ selector: SelectorSchema,
1795
1815
  action: ActionSchema
1796
1816
  });
1797
1817
  var RestoreStepsSchema = z5.array(ActStepSchema).min(1).max(64).optional();
@@ -1831,12 +1851,12 @@ var AuditUiArgs = z5.strictObject({
1831
1851
  minHitTargetPt: OptionalHitTargetSchema
1832
1852
  });
1833
1853
  var ActArgs = z5.strictObject({
1834
- selector: ShellSelectorSchema,
1854
+ selector: SelectorSchema,
1835
1855
  action: ActionSchema,
1836
1856
  degrade: z5.boolean().optional()
1837
1857
  });
1838
1858
  var AssertElementArgs = z5.strictObject({
1839
- selector: ShellSelectorSchema,
1859
+ selector: SelectorSchema,
1840
1860
  property: AssertionPropertySchema,
1841
1861
  expected: ExpectSchema
1842
1862
  });
@@ -1880,9 +1900,11 @@ var selectorJsonSchema = {
1880
1900
  type: "object",
1881
1901
  additionalProperties: false,
1882
1902
  properties: {
1883
- // `minLength` here is the advertised half of {@link ShellSelectorSchema};
1903
+ // `minLength` here is the advertised half of the kernel's `SelectorSchema`;
1884
1904
  // `test/tools.test.mjs` compares the two against each other and against the
1885
- // daemon's own rule, so neither side can be tightened or loosened alone.
1905
+ // daemon's own rule, and `test/consumer-consistency.test.mjs` compares both
1906
+ // against the published `kernel/schemas/evidence-pack.schema.json`, so no
1907
+ // side can be tightened or loosened alone.
1886
1908
  role: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH },
1887
1909
  title: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH },
1888
1910
  identifier: { type: "string", maxLength: SELECTOR_MAX_LENGTH }
@@ -2166,6 +2188,25 @@ var TOOL_SPECS = [
2166
2188
  var TOOL_BY_NAME = new Map(
2167
2189
  TOOL_SPECS.map((spec) => [spec.name, spec])
2168
2190
  );
2191
+ var TARGET_SELECTING_KNOBS = {
2192
+ gp_act: {
2193
+ param: "selector",
2194
+ picks: "which control the action is performed on",
2195
+ substitute: "a different control takes the press, and the user's screen changes in a way no one asked for"
2196
+ },
2197
+ gp_assert_element: {
2198
+ param: "selector",
2199
+ picks: "which element the claim is about",
2200
+ substitute: "the answer then describes a different element, while this session's trail still files it as the verdict on the claim you first made"
2201
+ }
2202
+ };
2203
+ function targetSelectingKnob(specName, paramName) {
2204
+ if (!Object.prototype.hasOwnProperty.call(TARGET_SELECTING_KNOBS, specName)) {
2205
+ return null;
2206
+ }
2207
+ const entry = TARGET_SELECTING_KNOBS[specName];
2208
+ return entry !== void 0 && entry.param === paramName ? { picks: entry.picks, substitute: entry.substitute } : null;
2209
+ }
2169
2210
  function narrowingKnobsFor(spec, params) {
2170
2211
  const properties = spec.inputSchema.properties ?? {};
2171
2212
  const knobs = [];
@@ -2180,11 +2221,21 @@ function narrowingKnobsFor(spec, params) {
2180
2221
  } else if (name === "limit") {
2181
2222
  knobs.push({ name, kind: "limit", minimum: schema.minimum, maximum: schema.maximum, sent });
2182
2223
  } else if (name === "selector" || name === "role" || name === "title") {
2183
- knobs.push({ name, kind: "selector", sent: void 0 });
2224
+ const target = targetSelectingKnob(spec.name, name);
2225
+ knobs.push(target === null ? { name, kind: "selector", sent: void 0 } : { name, kind: "target", sent: void 0, picks: target.picks, substitute: target.substitute });
2184
2226
  }
2185
2227
  }
2186
2228
  return knobs;
2187
2229
  }
2230
+ function replyShrinkingKnobs(knobs) {
2231
+ return knobs.filter((knob) => knob.kind !== "target");
2232
+ }
2233
+ function targetKnobs(knobs) {
2234
+ return knobs.filter((knob) => knob.kind === "target");
2235
+ }
2236
+ function describeTargetKnob(knob) {
2237
+ return `'${knob.name}' names ${knob.picks ?? "which element this operation is about"}, so changing it is not a smaller or cheaper request \u2014 it is a different operation, and ${knob.substitute ?? "the answer then describes something else"}`;
2238
+ }
2188
2239
  function atAdvertisedFloor(knob) {
2189
2240
  return knob.minimum !== void 0 && knob.sent !== void 0 && knob.sent <= knob.minimum;
2190
2241
  }
@@ -2198,6 +2249,8 @@ function knobSubject(kind) {
2198
2249
  return "the image";
2199
2250
  case "selector":
2200
2251
  return "the reply";
2252
+ case "target":
2253
+ return "the element the operation is about";
2201
2254
  }
2202
2255
  }
2203
2256
  function describeKnob(knob) {
@@ -2214,19 +2267,27 @@ function describeKnob(knob) {
2214
2267
  return knob.name === "selector" ? "a more specific selector (aim at one element)" : `a tighter ${knob.name} filter`;
2215
2268
  case "scale":
2216
2269
  return `a smaller scale${bounds}${knob.sent !== void 0 ? `, currently ${knob.sent}` : ""}`;
2270
+ case "target":
2271
+ return describeTargetKnob(knob);
2217
2272
  }
2218
2273
  }
2219
2274
  function oversizedReplyAdvice(spec, params) {
2220
2275
  const knobs = narrowingKnobsFor(spec, params);
2221
2276
  const atFloor = knobs.filter(atAdvertisedFloor);
2222
- const shrinkable = knobs.filter((knob) => !atAdvertisedFloor(knob));
2277
+ const target = targetKnobs(knobs);
2278
+ const shrinkable = replyShrinkingKnobs(knobs).filter((knob) => !atAdvertisedFloor(knob));
2223
2279
  const head = `the reply was too large for one frame (${MAX_FRAME_BYTES}-byte limit); `;
2224
- const floorNote = atFloor.length === 0 ? "" : `${atFloor.map(describeKnob).join("; ")}; `;
2280
+ const refused = [...atFloor.map(describeKnob), ...target.map(describeTargetKnob)];
2281
+ const floorNote = refused.length === 0 ? "" : `${refused.join("; ")}; `;
2225
2282
  if (shrinkable.length === 0) {
2226
- return knobs.length > 0 ? head + `${atFloor.map(describeKnob).join("; ")} \u2014 nothing left to narrow on this request: ask a smaller question (one element, one region) rather than retrying it unchanged; the connection is intact and the daemon keeps answering other requests` : head + "this tool advertises no parameter that shrinks a reply, so do not retry it unchanged \u2014 ask a smaller question (one element, one region) instead; the connection is intact and the daemon keeps answering other requests";
2283
+ return knobs.length > 0 ? head + floorNote + "nothing left to narrow on this request: " + (target.length > 0 ? "this request already carries one element and one region, so read the tree with gp_observe and, if the element you meant is another one, make that its own claim rather than treating this answer as a size problem" : "ask a smaller question (one element, one region) rather than retrying it unchanged") + "; the connection is intact and the daemon keeps answering other requests" : head + "this tool advertises no parameter that shrinks a reply, so do not retry it unchanged \u2014 ask a smaller question (one element, one region) instead; the connection is intact and the daemon keeps answering other requests";
2227
2284
  }
2228
2285
  return head + floorNote + `retry with ${shrinkable.map(describeKnob).join(" and ")}; the reply body has to fit one frame, and a retried identical request will be dropped identically; the connection is intact and the daemon keeps answering other requests`;
2229
2286
  }
2287
+ var NOT_NARROWABLE_BY_RETRY = /* @__PURE__ */ new Set([
2288
+ ...REPLAY_UNSAFE_METHOD_NAMES,
2289
+ ...STATE_CHANGING_ON_RETRY_METHOD_NAMES
2290
+ ]);
2230
2291
  async function readTrail(session, turn, limit) {
2231
2292
  await turn.acquire();
2232
2293
  try {