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.
- package/dist/dispatch.d.ts +1 -1
- package/dist/engine-client.d.ts +28 -0
- package/dist/http-gateway-cli.js +82 -21
- package/dist/http-gateway.js +82 -21
- package/dist/index.js +340 -59
- package/dist/tools.d.ts +5 -2
- package/dist/update-state.d.ts +60 -0
- package/package.json +1 -1
- package/schemas/evidence-pack.schema.json +2 -0
package/dist/dispatch.d.ts
CHANGED
|
@@ -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.
|
|
29
|
+
readonly version: "1.5.0";
|
|
30
30
|
};
|
|
31
31
|
export declare const PARSE_ERROR = -32700;
|
|
32
32
|
export declare const INVALID_REQUEST = -32600;
|
package/dist/engine-client.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/http-gateway-cli.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
1864
|
+
selector: SelectorSchema,
|
|
1845
1865
|
action: ActionSchema,
|
|
1846
1866
|
degrade: z5.boolean().optional()
|
|
1847
1867
|
});
|
|
1848
1868
|
var AssertElementArgs = z5.strictObject({
|
|
1849
|
-
selector:
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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 +
|
|
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 {
|
package/dist/http-gateway.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
1854
|
+
selector: SelectorSchema,
|
|
1835
1855
|
action: ActionSchema,
|
|
1836
1856
|
degrade: z5.boolean().optional()
|
|
1837
1857
|
});
|
|
1838
1858
|
var AssertElementArgs = z5.strictObject({
|
|
1839
|
-
selector:
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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 +
|
|
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 {
|