glasspane-mcp 1.3.1 → 1.4.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 +78 -21
- package/dist/http-gateway.js +78 -21
- package/dist/index.js +227 -59
- package/dist/tools.d.ts +5 -2
- package/dist/update-state.d.ts +25 -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.4.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([
|
|
@@ -1794,14 +1815,9 @@ var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
|
1794
1815
|
var OptionalOperationIdSchema = z5.string().regex(OPERATION_ID_PATTERN2).optional();
|
|
1795
1816
|
var OptionalProjectIdSchema = z5.string().regex(/^prj_[0-9A-HJKMNP-TV-Z]{26}$/).optional();
|
|
1796
1817
|
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
1818
|
var SnapshotIdSchema = z5.string().regex(/^snap_[0-9A-HJKMNP-TV-Z]{26}$/);
|
|
1803
1819
|
var ActStepSchema = z5.strictObject({
|
|
1804
|
-
selector:
|
|
1820
|
+
selector: SelectorSchema,
|
|
1805
1821
|
action: ActionSchema
|
|
1806
1822
|
});
|
|
1807
1823
|
var RestoreStepsSchema = z5.array(ActStepSchema).min(1).max(64).optional();
|
|
@@ -1841,12 +1857,12 @@ var AuditUiArgs = z5.strictObject({
|
|
|
1841
1857
|
minHitTargetPt: OptionalHitTargetSchema
|
|
1842
1858
|
});
|
|
1843
1859
|
var ActArgs = z5.strictObject({
|
|
1844
|
-
selector:
|
|
1860
|
+
selector: SelectorSchema,
|
|
1845
1861
|
action: ActionSchema,
|
|
1846
1862
|
degrade: z5.boolean().optional()
|
|
1847
1863
|
});
|
|
1848
1864
|
var AssertElementArgs = z5.strictObject({
|
|
1849
|
-
selector:
|
|
1865
|
+
selector: SelectorSchema,
|
|
1850
1866
|
property: AssertionPropertySchema,
|
|
1851
1867
|
expected: ExpectSchema
|
|
1852
1868
|
});
|
|
@@ -1890,9 +1906,11 @@ var selectorJsonSchema = {
|
|
|
1890
1906
|
type: "object",
|
|
1891
1907
|
additionalProperties: false,
|
|
1892
1908
|
properties: {
|
|
1893
|
-
// `minLength` here is the advertised half of
|
|
1909
|
+
// `minLength` here is the advertised half of the kernel's `SelectorSchema`;
|
|
1894
1910
|
// `test/tools.test.mjs` compares the two against each other and against the
|
|
1895
|
-
// daemon's own rule,
|
|
1911
|
+
// daemon's own rule, and `test/consumer-consistency.test.mjs` compares both
|
|
1912
|
+
// against the published `kernel/schemas/evidence-pack.schema.json`, so no
|
|
1913
|
+
// side can be tightened or loosened alone.
|
|
1896
1914
|
role: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH },
|
|
1897
1915
|
title: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH },
|
|
1898
1916
|
identifier: { type: "string", maxLength: SELECTOR_MAX_LENGTH }
|
|
@@ -2176,6 +2194,25 @@ var TOOL_SPECS = [
|
|
|
2176
2194
|
var TOOL_BY_NAME = new Map(
|
|
2177
2195
|
TOOL_SPECS.map((spec) => [spec.name, spec])
|
|
2178
2196
|
);
|
|
2197
|
+
var TARGET_SELECTING_KNOBS = {
|
|
2198
|
+
gp_act: {
|
|
2199
|
+
param: "selector",
|
|
2200
|
+
picks: "which control the action is performed on",
|
|
2201
|
+
substitute: "a different control takes the press, and the user's screen changes in a way no one asked for"
|
|
2202
|
+
},
|
|
2203
|
+
gp_assert_element: {
|
|
2204
|
+
param: "selector",
|
|
2205
|
+
picks: "which element the claim is about",
|
|
2206
|
+
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"
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
function targetSelectingKnob(specName, paramName) {
|
|
2210
|
+
if (!Object.prototype.hasOwnProperty.call(TARGET_SELECTING_KNOBS, specName)) {
|
|
2211
|
+
return null;
|
|
2212
|
+
}
|
|
2213
|
+
const entry = TARGET_SELECTING_KNOBS[specName];
|
|
2214
|
+
return entry !== void 0 && entry.param === paramName ? { picks: entry.picks, substitute: entry.substitute } : null;
|
|
2215
|
+
}
|
|
2179
2216
|
function narrowingKnobsFor(spec, params) {
|
|
2180
2217
|
const properties = spec.inputSchema.properties ?? {};
|
|
2181
2218
|
const knobs = [];
|
|
@@ -2190,11 +2227,21 @@ function narrowingKnobsFor(spec, params) {
|
|
|
2190
2227
|
} else if (name === "limit") {
|
|
2191
2228
|
knobs.push({ name, kind: "limit", minimum: schema.minimum, maximum: schema.maximum, sent });
|
|
2192
2229
|
} else if (name === "selector" || name === "role" || name === "title") {
|
|
2193
|
-
|
|
2230
|
+
const target = targetSelectingKnob(spec.name, name);
|
|
2231
|
+
knobs.push(target === null ? { name, kind: "selector", sent: void 0 } : { name, kind: "target", sent: void 0, picks: target.picks, substitute: target.substitute });
|
|
2194
2232
|
}
|
|
2195
2233
|
}
|
|
2196
2234
|
return knobs;
|
|
2197
2235
|
}
|
|
2236
|
+
function replyShrinkingKnobs(knobs) {
|
|
2237
|
+
return knobs.filter((knob) => knob.kind !== "target");
|
|
2238
|
+
}
|
|
2239
|
+
function targetKnobs(knobs) {
|
|
2240
|
+
return knobs.filter((knob) => knob.kind === "target");
|
|
2241
|
+
}
|
|
2242
|
+
function describeTargetKnob(knob) {
|
|
2243
|
+
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"}`;
|
|
2244
|
+
}
|
|
2198
2245
|
function atAdvertisedFloor(knob) {
|
|
2199
2246
|
return knob.minimum !== void 0 && knob.sent !== void 0 && knob.sent <= knob.minimum;
|
|
2200
2247
|
}
|
|
@@ -2208,6 +2255,8 @@ function knobSubject(kind) {
|
|
|
2208
2255
|
return "the image";
|
|
2209
2256
|
case "selector":
|
|
2210
2257
|
return "the reply";
|
|
2258
|
+
case "target":
|
|
2259
|
+
return "the element the operation is about";
|
|
2211
2260
|
}
|
|
2212
2261
|
}
|
|
2213
2262
|
function describeKnob(knob) {
|
|
@@ -2224,19 +2273,27 @@ function describeKnob(knob) {
|
|
|
2224
2273
|
return knob.name === "selector" ? "a more specific selector (aim at one element)" : `a tighter ${knob.name} filter`;
|
|
2225
2274
|
case "scale":
|
|
2226
2275
|
return `a smaller scale${bounds}${knob.sent !== void 0 ? `, currently ${knob.sent}` : ""}`;
|
|
2276
|
+
case "target":
|
|
2277
|
+
return describeTargetKnob(knob);
|
|
2227
2278
|
}
|
|
2228
2279
|
}
|
|
2229
2280
|
function oversizedReplyAdvice(spec, params) {
|
|
2230
2281
|
const knobs = narrowingKnobsFor(spec, params);
|
|
2231
2282
|
const atFloor = knobs.filter(atAdvertisedFloor);
|
|
2232
|
-
const
|
|
2283
|
+
const target = targetKnobs(knobs);
|
|
2284
|
+
const shrinkable = replyShrinkingKnobs(knobs).filter((knob) => !atAdvertisedFloor(knob));
|
|
2233
2285
|
const head = `the reply was too large for one frame (${MAX_FRAME_BYTES}-byte limit); `;
|
|
2234
|
-
const
|
|
2286
|
+
const refused = [...atFloor.map(describeKnob), ...target.map(describeTargetKnob)];
|
|
2287
|
+
const floorNote = refused.length === 0 ? "" : `${refused.join("; ")}; `;
|
|
2235
2288
|
if (shrinkable.length === 0) {
|
|
2236
|
-
return knobs.length > 0 ? head +
|
|
2289
|
+
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
2290
|
}
|
|
2238
2291
|
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
2292
|
}
|
|
2293
|
+
var NOT_NARROWABLE_BY_RETRY = /* @__PURE__ */ new Set([
|
|
2294
|
+
...REPLAY_UNSAFE_METHOD_NAMES,
|
|
2295
|
+
...STATE_CHANGING_ON_RETRY_METHOD_NAMES
|
|
2296
|
+
]);
|
|
2240
2297
|
async function readTrail(session, turn, limit) {
|
|
2241
2298
|
await turn.acquire();
|
|
2242
2299
|
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([
|
|
@@ -1784,14 +1805,9 @@ var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
|
1784
1805
|
var OptionalOperationIdSchema = z5.string().regex(OPERATION_ID_PATTERN2).optional();
|
|
1785
1806
|
var OptionalProjectIdSchema = z5.string().regex(/^prj_[0-9A-HJKMNP-TV-Z]{26}$/).optional();
|
|
1786
1807
|
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
1808
|
var SnapshotIdSchema = z5.string().regex(/^snap_[0-9A-HJKMNP-TV-Z]{26}$/);
|
|
1793
1809
|
var ActStepSchema = z5.strictObject({
|
|
1794
|
-
selector:
|
|
1810
|
+
selector: SelectorSchema,
|
|
1795
1811
|
action: ActionSchema
|
|
1796
1812
|
});
|
|
1797
1813
|
var RestoreStepsSchema = z5.array(ActStepSchema).min(1).max(64).optional();
|
|
@@ -1831,12 +1847,12 @@ var AuditUiArgs = z5.strictObject({
|
|
|
1831
1847
|
minHitTargetPt: OptionalHitTargetSchema
|
|
1832
1848
|
});
|
|
1833
1849
|
var ActArgs = z5.strictObject({
|
|
1834
|
-
selector:
|
|
1850
|
+
selector: SelectorSchema,
|
|
1835
1851
|
action: ActionSchema,
|
|
1836
1852
|
degrade: z5.boolean().optional()
|
|
1837
1853
|
});
|
|
1838
1854
|
var AssertElementArgs = z5.strictObject({
|
|
1839
|
-
selector:
|
|
1855
|
+
selector: SelectorSchema,
|
|
1840
1856
|
property: AssertionPropertySchema,
|
|
1841
1857
|
expected: ExpectSchema
|
|
1842
1858
|
});
|
|
@@ -1880,9 +1896,11 @@ var selectorJsonSchema = {
|
|
|
1880
1896
|
type: "object",
|
|
1881
1897
|
additionalProperties: false,
|
|
1882
1898
|
properties: {
|
|
1883
|
-
// `minLength` here is the advertised half of
|
|
1899
|
+
// `minLength` here is the advertised half of the kernel's `SelectorSchema`;
|
|
1884
1900
|
// `test/tools.test.mjs` compares the two against each other and against the
|
|
1885
|
-
// daemon's own rule,
|
|
1901
|
+
// daemon's own rule, and `test/consumer-consistency.test.mjs` compares both
|
|
1902
|
+
// against the published `kernel/schemas/evidence-pack.schema.json`, so no
|
|
1903
|
+
// side can be tightened or loosened alone.
|
|
1886
1904
|
role: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH },
|
|
1887
1905
|
title: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH },
|
|
1888
1906
|
identifier: { type: "string", maxLength: SELECTOR_MAX_LENGTH }
|
|
@@ -2166,6 +2184,25 @@ var TOOL_SPECS = [
|
|
|
2166
2184
|
var TOOL_BY_NAME = new Map(
|
|
2167
2185
|
TOOL_SPECS.map((spec) => [spec.name, spec])
|
|
2168
2186
|
);
|
|
2187
|
+
var TARGET_SELECTING_KNOBS = {
|
|
2188
|
+
gp_act: {
|
|
2189
|
+
param: "selector",
|
|
2190
|
+
picks: "which control the action is performed on",
|
|
2191
|
+
substitute: "a different control takes the press, and the user's screen changes in a way no one asked for"
|
|
2192
|
+
},
|
|
2193
|
+
gp_assert_element: {
|
|
2194
|
+
param: "selector",
|
|
2195
|
+
picks: "which element the claim is about",
|
|
2196
|
+
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"
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
function targetSelectingKnob(specName, paramName) {
|
|
2200
|
+
if (!Object.prototype.hasOwnProperty.call(TARGET_SELECTING_KNOBS, specName)) {
|
|
2201
|
+
return null;
|
|
2202
|
+
}
|
|
2203
|
+
const entry = TARGET_SELECTING_KNOBS[specName];
|
|
2204
|
+
return entry !== void 0 && entry.param === paramName ? { picks: entry.picks, substitute: entry.substitute } : null;
|
|
2205
|
+
}
|
|
2169
2206
|
function narrowingKnobsFor(spec, params) {
|
|
2170
2207
|
const properties = spec.inputSchema.properties ?? {};
|
|
2171
2208
|
const knobs = [];
|
|
@@ -2180,11 +2217,21 @@ function narrowingKnobsFor(spec, params) {
|
|
|
2180
2217
|
} else if (name === "limit") {
|
|
2181
2218
|
knobs.push({ name, kind: "limit", minimum: schema.minimum, maximum: schema.maximum, sent });
|
|
2182
2219
|
} else if (name === "selector" || name === "role" || name === "title") {
|
|
2183
|
-
|
|
2220
|
+
const target = targetSelectingKnob(spec.name, name);
|
|
2221
|
+
knobs.push(target === null ? { name, kind: "selector", sent: void 0 } : { name, kind: "target", sent: void 0, picks: target.picks, substitute: target.substitute });
|
|
2184
2222
|
}
|
|
2185
2223
|
}
|
|
2186
2224
|
return knobs;
|
|
2187
2225
|
}
|
|
2226
|
+
function replyShrinkingKnobs(knobs) {
|
|
2227
|
+
return knobs.filter((knob) => knob.kind !== "target");
|
|
2228
|
+
}
|
|
2229
|
+
function targetKnobs(knobs) {
|
|
2230
|
+
return knobs.filter((knob) => knob.kind === "target");
|
|
2231
|
+
}
|
|
2232
|
+
function describeTargetKnob(knob) {
|
|
2233
|
+
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"}`;
|
|
2234
|
+
}
|
|
2188
2235
|
function atAdvertisedFloor(knob) {
|
|
2189
2236
|
return knob.minimum !== void 0 && knob.sent !== void 0 && knob.sent <= knob.minimum;
|
|
2190
2237
|
}
|
|
@@ -2198,6 +2245,8 @@ function knobSubject(kind) {
|
|
|
2198
2245
|
return "the image";
|
|
2199
2246
|
case "selector":
|
|
2200
2247
|
return "the reply";
|
|
2248
|
+
case "target":
|
|
2249
|
+
return "the element the operation is about";
|
|
2201
2250
|
}
|
|
2202
2251
|
}
|
|
2203
2252
|
function describeKnob(knob) {
|
|
@@ -2214,19 +2263,27 @@ function describeKnob(knob) {
|
|
|
2214
2263
|
return knob.name === "selector" ? "a more specific selector (aim at one element)" : `a tighter ${knob.name} filter`;
|
|
2215
2264
|
case "scale":
|
|
2216
2265
|
return `a smaller scale${bounds}${knob.sent !== void 0 ? `, currently ${knob.sent}` : ""}`;
|
|
2266
|
+
case "target":
|
|
2267
|
+
return describeTargetKnob(knob);
|
|
2217
2268
|
}
|
|
2218
2269
|
}
|
|
2219
2270
|
function oversizedReplyAdvice(spec, params) {
|
|
2220
2271
|
const knobs = narrowingKnobsFor(spec, params);
|
|
2221
2272
|
const atFloor = knobs.filter(atAdvertisedFloor);
|
|
2222
|
-
const
|
|
2273
|
+
const target = targetKnobs(knobs);
|
|
2274
|
+
const shrinkable = replyShrinkingKnobs(knobs).filter((knob) => !atAdvertisedFloor(knob));
|
|
2223
2275
|
const head = `the reply was too large for one frame (${MAX_FRAME_BYTES}-byte limit); `;
|
|
2224
|
-
const
|
|
2276
|
+
const refused = [...atFloor.map(describeKnob), ...target.map(describeTargetKnob)];
|
|
2277
|
+
const floorNote = refused.length === 0 ? "" : `${refused.join("; ")}; `;
|
|
2225
2278
|
if (shrinkable.length === 0) {
|
|
2226
|
-
return knobs.length > 0 ? head +
|
|
2279
|
+
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
2280
|
}
|
|
2228
2281
|
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
2282
|
}
|
|
2283
|
+
var NOT_NARROWABLE_BY_RETRY = /* @__PURE__ */ new Set([
|
|
2284
|
+
...REPLAY_UNSAFE_METHOD_NAMES,
|
|
2285
|
+
...STATE_CHANGING_ON_RETRY_METHOD_NAMES
|
|
2286
|
+
]);
|
|
2230
2287
|
async function readTrail(session, turn, limit) {
|
|
2231
2288
|
await turn.acquire();
|
|
2232
2289
|
try {
|
package/dist/index.js
CHANGED
|
@@ -46,8 +46,8 @@ var LEGACY_EVIDENCE_SCHEMA_VERSION = "glasspane.evidence/0.1-draft";
|
|
|
46
46
|
var AttributionLevelSchema = z.enum(["soft", "strong", "weak"]);
|
|
47
47
|
var CircuitBreakerLevelSchema = z.number().int().min(0).max(3);
|
|
48
48
|
var SelectorSchema = z.strictObject({
|
|
49
|
-
role: z.string().max(SELECTOR_MAX_LENGTH),
|
|
50
|
-
title: z.string().max(SELECTOR_MAX_LENGTH).optional(),
|
|
49
|
+
role: z.string().min(1).max(SELECTOR_MAX_LENGTH),
|
|
50
|
+
title: z.string().min(1).max(SELECTOR_MAX_LENGTH).optional(),
|
|
51
51
|
identifier: z.string().max(SELECTOR_MAX_LENGTH).optional()
|
|
52
52
|
});
|
|
53
53
|
var ActionSchema = z.enum([
|
|
@@ -265,6 +265,107 @@ function parseEvidencePackRead(input) {
|
|
|
265
265
|
throw schemaError("evidence pack", result.error);
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
// src/update-state.ts
|
|
269
|
+
import fs from "node:fs";
|
|
270
|
+
import os from "node:os";
|
|
271
|
+
import path from "node:path";
|
|
272
|
+
var STATE_DIR_ENV = "GLASSPANE_STATE_DIR";
|
|
273
|
+
function recordHome() {
|
|
274
|
+
try {
|
|
275
|
+
return os.userInfo().homedir;
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function updateStateFile(env = process.env, stateRootOverride) {
|
|
281
|
+
const named2 = stateRootOverride ?? env[STATE_DIR_ENV];
|
|
282
|
+
if (typeof named2 === "string" && named2.trim() !== "") {
|
|
283
|
+
return path.join(path.resolve(named2), "update-state.json");
|
|
284
|
+
}
|
|
285
|
+
const home = recordHome();
|
|
286
|
+
if (home === null) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return path.join(home, ".glasspane", "update-state.json");
|
|
290
|
+
}
|
|
291
|
+
var KNOWN_STATUSES = /* @__PURE__ */ new Set([
|
|
292
|
+
"up-to-date",
|
|
293
|
+
"available",
|
|
294
|
+
"staged",
|
|
295
|
+
"applied",
|
|
296
|
+
"deferred",
|
|
297
|
+
"needs-consent",
|
|
298
|
+
"check-overdue",
|
|
299
|
+
"rolled-back",
|
|
300
|
+
"check-failed",
|
|
301
|
+
"staged-build-failed",
|
|
302
|
+
"apply-failed",
|
|
303
|
+
"rollback-failed",
|
|
304
|
+
"disabled",
|
|
305
|
+
"installer-pointer-stale"
|
|
306
|
+
]);
|
|
307
|
+
function readUpdateState(env = process.env, stateRootOverride) {
|
|
308
|
+
const stateFile = updateStateFile(env, stateRootOverride);
|
|
309
|
+
if (stateFile === null) {
|
|
310
|
+
return {
|
|
311
|
+
kind: "unresolvable",
|
|
312
|
+
stateFile: null,
|
|
313
|
+
summary: "update state: this process has no password-database record, so the state root cannot be named; nothing was read and no claim about GlassPane's freshness is being made"
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (!fs.existsSync(stateFile)) {
|
|
317
|
+
return {
|
|
318
|
+
kind: "absent",
|
|
319
|
+
stateFile,
|
|
320
|
+
summary: `update state: no ${stateFile} \u2014 auto-update has never run on this machine, so nothing here says the installed GlassPane is current; run \`node updater/cli.js check\` (or use the settings panel) to find out`
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
let text;
|
|
324
|
+
try {
|
|
325
|
+
text = fs.readFileSync(stateFile, "utf8");
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return {
|
|
328
|
+
kind: "unreadable",
|
|
329
|
+
stateFile,
|
|
330
|
+
summary: `update state: ${stateFile} exists but could not be read (${String(error)}); freshness is unknown, which is not the same as up to date`
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
let parsed;
|
|
334
|
+
try {
|
|
335
|
+
parsed = JSON.parse(text);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return {
|
|
338
|
+
kind: "unreadable",
|
|
339
|
+
stateFile,
|
|
340
|
+
summary: `update state: ${stateFile} is not valid JSON (${String(error)}); freshness is unknown, which is not the same as up to date`
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const status = typeof parsed.status === "string" ? parsed.status : null;
|
|
344
|
+
if (status === null || !KNOWN_STATUSES.has(status)) {
|
|
345
|
+
return {
|
|
346
|
+
kind: "unreadable",
|
|
347
|
+
stateFile,
|
|
348
|
+
summary: `update state: ${stateFile} carries status ${JSON.stringify(parsed.status ?? null)}, which is not a status this shell knows; the updater and this shell disagree about the contract, so freshness is unknown`
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const bits = [
|
|
352
|
+
`status=${status}`,
|
|
353
|
+
`current=${String(parsed.current ?? "unknown")}`,
|
|
354
|
+
`latest=${String(parsed.latest ?? "none-seen")}`,
|
|
355
|
+
`lastCheckAt=${String(parsed.updatedAt ?? "never")}`,
|
|
356
|
+
`autoUpdate=${parsed.disabled === true ? "off" : parsed.autoApply === true ? "on" : "on-by-default"}`
|
|
357
|
+
];
|
|
358
|
+
const lastError = parsed.lastError;
|
|
359
|
+
if (lastError && typeof lastError.message === "string") {
|
|
360
|
+
bits.push(`lastError=${lastError.code ?? "no-code"}: ${lastError.message}`);
|
|
361
|
+
}
|
|
362
|
+
const staged = parsed.staged;
|
|
363
|
+
if (staged && typeof staged.version === "string") {
|
|
364
|
+
bits.push(`staged=${staged.version}${staged.digest ? ` (${String(staged.digest).slice(0, 12)}\u2026)` : ""}`);
|
|
365
|
+
}
|
|
366
|
+
return { kind: "read", stateFile, summary: `update state: ${bits.join("; ")}` };
|
|
367
|
+
}
|
|
368
|
+
|
|
268
369
|
// src/io.ts
|
|
269
370
|
var MAX_FRAME_BYTES = 4 * 1024 * 1024;
|
|
270
371
|
var OVERSIZE_PREFIX_BYTES = 512;
|
|
@@ -497,9 +598,9 @@ var DrainAwareWriter = class {
|
|
|
497
598
|
|
|
498
599
|
// src/engine-client.ts
|
|
499
600
|
import net from "node:net";
|
|
500
|
-
import
|
|
501
|
-
import
|
|
502
|
-
import
|
|
601
|
+
import os2 from "node:os";
|
|
602
|
+
import path2 from "node:path";
|
|
603
|
+
import fs2 from "node:fs";
|
|
503
604
|
import { fileURLToPath } from "node:url";
|
|
504
605
|
|
|
505
606
|
// src/errors.ts
|
|
@@ -522,16 +623,16 @@ function formatToolErrorShape(error) {
|
|
|
522
623
|
|
|
523
624
|
// src/engine-client.ts
|
|
524
625
|
function daemonUnreachableRemedy() {
|
|
525
|
-
const here =
|
|
526
|
-
const installerCli =
|
|
527
|
-
if (
|
|
626
|
+
const here = path2.dirname(fileURLToPath(import.meta.url));
|
|
627
|
+
const installerCli = path2.resolve(here, "..", "..", "installer", "cli.js");
|
|
628
|
+
if (fs2.existsSync(installerCli)) {
|
|
528
629
|
return `run "node ${installerCli} --restore-launchd" (auto-restores and verifies the launchd-managed daemon), then retry`;
|
|
529
630
|
}
|
|
530
631
|
return "ensure the glasspane daemon is running (launchd job com.glasspane.daemon; in a repo checkout run `node installer/cli.js --restore-launchd`), then retry";
|
|
531
632
|
}
|
|
532
633
|
var ENGINE_SOCKET_ENV = "GLASSPANE_ENGINE_SOCK";
|
|
533
634
|
function defaultSocketPath() {
|
|
534
|
-
return process.env[ENGINE_SOCKET_ENV] ??
|
|
635
|
+
return process.env[ENGINE_SOCKET_ENV] ?? path2.join(os2.homedir(), ".glasspane", "engine.sock");
|
|
535
636
|
}
|
|
536
637
|
var ENGINE_TIMEOUT_MS = 1e4;
|
|
537
638
|
var CALLER_VISIBLE_CEILING_MS = 5e4;
|
|
@@ -582,6 +683,7 @@ function isReplayUnsafeMethod(method) {
|
|
|
582
683
|
return Object.prototype.hasOwnProperty.call(REPLAY_UNSAFE_REASONS, method);
|
|
583
684
|
}
|
|
584
685
|
var REPLAY_UNSAFE_METHOD_NAMES = Object.keys(REPLAY_UNSAFE_REASONS);
|
|
686
|
+
var STATE_CHANGING_ON_RETRY_METHOD_NAMES = ["snapshot"];
|
|
585
687
|
function engineDeadlineMs(method, params) {
|
|
586
688
|
if (method === "restore") {
|
|
587
689
|
const steps = params?.steps;
|
|
@@ -638,6 +740,10 @@ var NARROWING_AUTHORITY = {
|
|
|
638
740
|
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",
|
|
639
741
|
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"
|
|
640
742
|
};
|
|
743
|
+
function clientClosingRemedy(surface) {
|
|
744
|
+
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";
|
|
745
|
+
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.`;
|
|
746
|
+
}
|
|
641
747
|
function reIssueGuidance(surface) {
|
|
642
748
|
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`;
|
|
643
749
|
}
|
|
@@ -793,8 +899,8 @@ var EngineJsonRpcClient = class {
|
|
|
793
899
|
this.state.closed = true;
|
|
794
900
|
this.teardown(new EngineCallError(
|
|
795
901
|
GP_E_ENGINE_UNREACHABLE,
|
|
796
|
-
"engine client closed",
|
|
797
|
-
|
|
902
|
+
"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",
|
|
903
|
+
clientClosingRemedy(this.surface)
|
|
798
904
|
));
|
|
799
905
|
this.io.close();
|
|
800
906
|
}
|
|
@@ -843,7 +949,11 @@ var EngineJsonRpcClient = class {
|
|
|
843
949
|
if (typeof id === "number") {
|
|
844
950
|
const entry = this.pending.get(id);
|
|
845
951
|
if (!entry) {
|
|
846
|
-
|
|
952
|
+
const tracked = [...this.pending.values()];
|
|
953
|
+
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(", ");
|
|
954
|
+
this.report(
|
|
955
|
+
`engine sent a frame for request id ${id}, which this client never issued (or already gave up on); ` + held + ": " + truncate(line)
|
|
956
|
+
);
|
|
847
957
|
return;
|
|
848
958
|
}
|
|
849
959
|
this.pending.delete(id);
|
|
@@ -858,8 +968,9 @@ var EngineJsonRpcClient = class {
|
|
|
858
968
|
const target = this.oldestTracked();
|
|
859
969
|
if (target) {
|
|
860
970
|
if (target.settled) {
|
|
861
|
-
this.pending.delete(target.id);
|
|
862
971
|
this.reportLateReply(target, frame, true);
|
|
972
|
+
target.arrivalOrderConsumed = true;
|
|
973
|
+
this.retainForLateReply();
|
|
863
974
|
return;
|
|
864
975
|
}
|
|
865
976
|
if (frame.error) {
|
|
@@ -920,9 +1031,20 @@ var EngineJsonRpcClient = class {
|
|
|
920
1031
|
* Oldest request this client still tracks, settled ones included: that is the
|
|
921
1032
|
* request the daemon's arrival-ordered answer belongs to. `Map` iteration is
|
|
922
1033
|
* insertion order, which is the order the requests went out.
|
|
1034
|
+
*
|
|
1035
|
+
* An entry whose id-less answer has already been taken (`arrivalOrderConsumed`)
|
|
1036
|
+
* is passed over for *this* purpose while staying in the map for the echoed-id
|
|
1037
|
+
* lookup: the guess has been spent once and may not be spent again on the next
|
|
1038
|
+
* id-less frame, and the request it was spent on is still the request whose real
|
|
1039
|
+
* reply can arrive with its id (finding 中-4).
|
|
923
1040
|
*/
|
|
924
1041
|
oldestTracked() {
|
|
925
|
-
|
|
1042
|
+
for (const entry of this.pending.values()) {
|
|
1043
|
+
if (!entry.arrivalOrderConsumed) {
|
|
1044
|
+
return entry;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return void 0;
|
|
926
1048
|
}
|
|
927
1049
|
/**
|
|
928
1050
|
* Route the reply that lands after its caller was already answered. `inferred`
|
|
@@ -1612,9 +1734,9 @@ function exponentialFromDigits(digits, power) {
|
|
|
1612
1734
|
}
|
|
1613
1735
|
|
|
1614
1736
|
// src/project-registry.ts
|
|
1615
|
-
import
|
|
1616
|
-
import
|
|
1617
|
-
import
|
|
1737
|
+
import fs3 from "node:fs";
|
|
1738
|
+
import os3 from "node:os";
|
|
1739
|
+
import path3 from "node:path";
|
|
1618
1740
|
import { randomBytes } from "node:crypto";
|
|
1619
1741
|
import { z as z4 } from "zod";
|
|
1620
1742
|
var CROCKFORD_BODY_RE = "[0-9A-HJKMNP-TV-Z]{26}";
|
|
@@ -1653,7 +1775,7 @@ var PROTECTED_SUBTREES = [
|
|
|
1653
1775
|
];
|
|
1654
1776
|
function systemHome() {
|
|
1655
1777
|
try {
|
|
1656
|
-
return
|
|
1778
|
+
return os3.userInfo().homedir;
|
|
1657
1779
|
} catch (error) {
|
|
1658
1780
|
throw new ProjectRegistryError(
|
|
1659
1781
|
// Not GP_E_INTERNAL: `tools.ts` maps that code to "the projects file is
|
|
@@ -1665,7 +1787,7 @@ function systemHome() {
|
|
|
1665
1787
|
}
|
|
1666
1788
|
}
|
|
1667
1789
|
function daemonProjectsPath() {
|
|
1668
|
-
return
|
|
1790
|
+
return path3.join(systemHome(), ".glasspane", "projects.json");
|
|
1669
1791
|
}
|
|
1670
1792
|
var FIRMLINK_PREFIX = "/System/Volumes/Data";
|
|
1671
1793
|
var FORCE_OVERWRITE_ENV = "GLASSPANE_PROJECTS_FORCE_OVERWRITE";
|
|
@@ -1696,7 +1818,7 @@ function projectsPath() {
|
|
|
1696
1818
|
function loadRegistry(filePath) {
|
|
1697
1819
|
let raw;
|
|
1698
1820
|
try {
|
|
1699
|
-
raw =
|
|
1821
|
+
raw = fs3.readFileSync(filePath, "utf8");
|
|
1700
1822
|
} catch (error) {
|
|
1701
1823
|
if (nodeErrorCode(error) === "ENOENT") {
|
|
1702
1824
|
return { projects: [], failure: null };
|
|
@@ -1765,7 +1887,7 @@ function requireUsableRegistry(filePath, snapshot) {
|
|
|
1765
1887
|
}
|
|
1766
1888
|
const discarded = `${filePath}.unreadable-${uniqueToken()}`;
|
|
1767
1889
|
try {
|
|
1768
|
-
|
|
1890
|
+
fs3.renameSync(filePath, discarded);
|
|
1769
1891
|
} catch (error) {
|
|
1770
1892
|
throw new ProjectRegistryError(
|
|
1771
1893
|
GP_E_INTERNAL,
|
|
@@ -1784,7 +1906,7 @@ var STATE_DIR_MODE = 448;
|
|
|
1784
1906
|
var STATE_FILE_MODE = 384;
|
|
1785
1907
|
function tightenMode(target, want, kind) {
|
|
1786
1908
|
try {
|
|
1787
|
-
|
|
1909
|
+
fs3.chmodSync(target, want);
|
|
1788
1910
|
} catch (error) {
|
|
1789
1911
|
throw new ProjectRegistryError(
|
|
1790
1912
|
GP_E_INTERNAL,
|
|
@@ -1793,7 +1915,7 @@ function tightenMode(target, want, kind) {
|
|
|
1793
1915
|
}
|
|
1794
1916
|
let mode;
|
|
1795
1917
|
try {
|
|
1796
|
-
mode =
|
|
1918
|
+
mode = fs3.statSync(target).mode & 511;
|
|
1797
1919
|
} catch (error) {
|
|
1798
1920
|
throw new ProjectRegistryError(
|
|
1799
1921
|
GP_E_INTERNAL,
|
|
@@ -1808,18 +1930,18 @@ function tightenMode(target, want, kind) {
|
|
|
1808
1930
|
}
|
|
1809
1931
|
}
|
|
1810
1932
|
function saveProjects(filePath, entries) {
|
|
1811
|
-
const dir =
|
|
1933
|
+
const dir = path3.dirname(filePath);
|
|
1812
1934
|
const tmpPath = `${filePath}.tmp-${uniqueToken()}`;
|
|
1813
1935
|
try {
|
|
1814
|
-
|
|
1936
|
+
fs3.mkdirSync(dir, { recursive: true, mode: STATE_DIR_MODE });
|
|
1815
1937
|
tightenMode(dir, STATE_DIR_MODE, "directory");
|
|
1816
|
-
|
|
1938
|
+
fs3.writeFileSync(tmpPath, JSON.stringify(entries, null, 2), "utf8");
|
|
1817
1939
|
tightenMode(tmpPath, STATE_FILE_MODE, "file");
|
|
1818
|
-
|
|
1940
|
+
fs3.renameSync(tmpPath, filePath);
|
|
1819
1941
|
tightenMode(filePath, STATE_FILE_MODE, "file");
|
|
1820
1942
|
} catch (error) {
|
|
1821
1943
|
try {
|
|
1822
|
-
|
|
1944
|
+
fs3.unlinkSync(tmpPath);
|
|
1823
1945
|
} catch {
|
|
1824
1946
|
}
|
|
1825
1947
|
if (error instanceof ProjectRegistryError) {
|
|
@@ -1927,7 +2049,7 @@ function assertSingleIdentity(entry) {
|
|
|
1927
2049
|
}
|
|
1928
2050
|
function daemonRestartNotice(filePath) {
|
|
1929
2051
|
const daemonPath = daemonProjectsPath();
|
|
1930
|
-
if (
|
|
2052
|
+
if (path3.resolve(filePath) !== path3.resolve(daemonPath)) {
|
|
1931
2053
|
return `Saved in ${filePath}. The background service does not read this path because of anything in its own configuration: its default root is the folder under this user's home record, where it loads ${daemonPath} once when it starts, and ${PROJECTS_FILE_ENV} \u2014 the variable that pointed this write here \u2014 is honoured by this MCP shell only, so as far as the service's own configuration goes it never reads that file, and no restart of the service will make it load this one. ${rootDiscriminator()} To land the entry where that default root looks: unset ${PROJECTS_FILE_ENV} for this shell (or point it at ${daemonPath}) and repeat gp_project_set, then restart the background service so it reloads \u2014 ${restartCommand()}`;
|
|
1932
2054
|
}
|
|
1933
2055
|
return `Saved in ${filePath}, the projects file under the home record this user's state root defaults to. If this is the root the service was started on \u2014 which is not knowable from here \u2014 then the running background service loaded that file once when it started, so gp_attach will keep returning GP_E_NOT_FOUND for this projectId until the service restarts: ${restartCommand()}. ${rootDiscriminator()}`;
|
|
@@ -1959,7 +2081,7 @@ function checkAgentPath(field, value) {
|
|
|
1959
2081
|
throw badPath(field, value, `must not contain "." or ".." components (got "${value}")`);
|
|
1960
2082
|
}
|
|
1961
2083
|
}
|
|
1962
|
-
const logical =
|
|
2084
|
+
const logical = path3.posix.normalize(value);
|
|
1963
2085
|
const given = stripFirmlink(logical);
|
|
1964
2086
|
const resolved = stripFirmlink(resolveExistingAncestor(logical));
|
|
1965
2087
|
const refusal = strongestRefusal(given === resolved ? [resolved] : [resolved, given], resolved);
|
|
@@ -2008,7 +2130,7 @@ function refuseHomeDotDirectory(target, home) {
|
|
|
2008
2130
|
return named(`a dot-directory of the current user's home directory (~/${first}), which is where that account's credentials and client configuration live (\`~/.ssh\`, \`~/.aws\`, \`~/.gnupg\`, \`~/.config\`) or the daemon's own private state (\`~/.glasspane\`); the daemon chmods a registered storage root to 0700 and writes evidence packs into it, and prunes expired ones out of it, so this is not a place a project archive may be pointed at \u2014 name a directory inside the project instead, e.g. "the project tree/${first}"`);
|
|
2009
2131
|
}
|
|
2010
2132
|
function refuseCandidate(target) {
|
|
2011
|
-
const home = stripFirmlink(
|
|
2133
|
+
const home = stripFirmlink(path3.posix.normalize(systemHome()));
|
|
2012
2134
|
if (target === home) return named("the current user's home directory");
|
|
2013
2135
|
if (isInside(`${home}/Library`, target)) return named("inside the current user's Library folder");
|
|
2014
2136
|
const homeDot = refuseHomeDotDirectory(target, home);
|
|
@@ -2037,7 +2159,7 @@ function refuseUnownedTree(target) {
|
|
|
2037
2159
|
}
|
|
2038
2160
|
let stat;
|
|
2039
2161
|
try {
|
|
2040
|
-
stat =
|
|
2162
|
+
stat = fs3.statSync(ancestor);
|
|
2041
2163
|
} catch (error) {
|
|
2042
2164
|
return `under a nearest existing directory ${ancestor} that cannot be inspected (${describeError(error)})`;
|
|
2043
2165
|
}
|
|
@@ -2052,10 +2174,10 @@ function refuseUnownedTree(target) {
|
|
|
2052
2174
|
function nearestExistingAncestor(target) {
|
|
2053
2175
|
let cursor = target;
|
|
2054
2176
|
for (; ; ) {
|
|
2055
|
-
if (
|
|
2177
|
+
if (fs3.existsSync(cursor)) {
|
|
2056
2178
|
return cursor;
|
|
2057
2179
|
}
|
|
2058
|
-
const parent =
|
|
2180
|
+
const parent = path3.posix.dirname(cursor);
|
|
2059
2181
|
if (parent === cursor) return parent;
|
|
2060
2182
|
cursor = parent;
|
|
2061
2183
|
}
|
|
@@ -2075,12 +2197,12 @@ function resolveExistingAncestor(target) {
|
|
|
2075
2197
|
const missingTail = [];
|
|
2076
2198
|
for (; ; ) {
|
|
2077
2199
|
try {
|
|
2078
|
-
const real =
|
|
2079
|
-
return missingTail.length === 0 ? real :
|
|
2200
|
+
const real = fs3.realpathSync(cursor);
|
|
2201
|
+
return missingTail.length === 0 ? real : path3.posix.join(real, ...missingTail);
|
|
2080
2202
|
} catch {
|
|
2081
|
-
const parent =
|
|
2203
|
+
const parent = path3.posix.dirname(cursor);
|
|
2082
2204
|
if (parent === cursor) return target;
|
|
2083
|
-
missingTail.unshift(
|
|
2205
|
+
missingTail.unshift(path3.posix.basename(cursor));
|
|
2084
2206
|
cursor = parent;
|
|
2085
2207
|
}
|
|
2086
2208
|
}
|
|
@@ -2126,14 +2248,9 @@ var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
|
2126
2248
|
var OptionalOperationIdSchema = z5.string().regex(OPERATION_ID_PATTERN2).optional();
|
|
2127
2249
|
var OptionalProjectIdSchema = z5.string().regex(/^prj_[0-9A-HJKMNP-TV-Z]{26}$/).optional();
|
|
2128
2250
|
var ExpectSchema = z5.union([z5.string().max(512), z5.boolean()]);
|
|
2129
|
-
var ShellSelectorSchema = z5.strictObject({
|
|
2130
|
-
role: z5.string().min(1).max(SELECTOR_MAX_LENGTH),
|
|
2131
|
-
title: z5.string().min(1).max(SELECTOR_MAX_LENGTH).optional(),
|
|
2132
|
-
identifier: z5.string().max(SELECTOR_MAX_LENGTH).optional()
|
|
2133
|
-
});
|
|
2134
2251
|
var SnapshotIdSchema = z5.string().regex(/^snap_[0-9A-HJKMNP-TV-Z]{26}$/);
|
|
2135
2252
|
var ActStepSchema = z5.strictObject({
|
|
2136
|
-
selector:
|
|
2253
|
+
selector: SelectorSchema,
|
|
2137
2254
|
action: ActionSchema
|
|
2138
2255
|
});
|
|
2139
2256
|
var RestoreStepsSchema = z5.array(ActStepSchema).min(1).max(64).optional();
|
|
@@ -2173,12 +2290,12 @@ var AuditUiArgs = z5.strictObject({
|
|
|
2173
2290
|
minHitTargetPt: OptionalHitTargetSchema
|
|
2174
2291
|
});
|
|
2175
2292
|
var ActArgs = z5.strictObject({
|
|
2176
|
-
selector:
|
|
2293
|
+
selector: SelectorSchema,
|
|
2177
2294
|
action: ActionSchema,
|
|
2178
2295
|
degrade: z5.boolean().optional()
|
|
2179
2296
|
});
|
|
2180
2297
|
var AssertElementArgs = z5.strictObject({
|
|
2181
|
-
selector:
|
|
2298
|
+
selector: SelectorSchema,
|
|
2182
2299
|
property: AssertionPropertySchema,
|
|
2183
2300
|
expected: ExpectSchema
|
|
2184
2301
|
});
|
|
@@ -2222,9 +2339,11 @@ var selectorJsonSchema = {
|
|
|
2222
2339
|
type: "object",
|
|
2223
2340
|
additionalProperties: false,
|
|
2224
2341
|
properties: {
|
|
2225
|
-
// `minLength` here is the advertised half of
|
|
2342
|
+
// `minLength` here is the advertised half of the kernel's `SelectorSchema`;
|
|
2226
2343
|
// `test/tools.test.mjs` compares the two against each other and against the
|
|
2227
|
-
// daemon's own rule,
|
|
2344
|
+
// daemon's own rule, and `test/consumer-consistency.test.mjs` compares both
|
|
2345
|
+
// against the published `kernel/schemas/evidence-pack.schema.json`, so no
|
|
2346
|
+
// side can be tightened or loosened alone.
|
|
2228
2347
|
role: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH },
|
|
2229
2348
|
title: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH },
|
|
2230
2349
|
identifier: { type: "string", maxLength: SELECTOR_MAX_LENGTH }
|
|
@@ -2538,6 +2657,25 @@ function attachIdentityOf(result) {
|
|
|
2538
2657
|
const bundleId = typeof record.bundleId === "string" ? record.bundleId : "<no-bundle-id>";
|
|
2539
2658
|
return `${pid}|${bundleId}|${appName}`;
|
|
2540
2659
|
}
|
|
2660
|
+
var TARGET_SELECTING_KNOBS = {
|
|
2661
|
+
gp_act: {
|
|
2662
|
+
param: "selector",
|
|
2663
|
+
picks: "which control the action is performed on",
|
|
2664
|
+
substitute: "a different control takes the press, and the user's screen changes in a way no one asked for"
|
|
2665
|
+
},
|
|
2666
|
+
gp_assert_element: {
|
|
2667
|
+
param: "selector",
|
|
2668
|
+
picks: "which element the claim is about",
|
|
2669
|
+
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"
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
function targetSelectingKnob(specName, paramName) {
|
|
2673
|
+
if (!Object.prototype.hasOwnProperty.call(TARGET_SELECTING_KNOBS, specName)) {
|
|
2674
|
+
return null;
|
|
2675
|
+
}
|
|
2676
|
+
const entry = TARGET_SELECTING_KNOBS[specName];
|
|
2677
|
+
return entry !== void 0 && entry.param === paramName ? { picks: entry.picks, substitute: entry.substitute } : null;
|
|
2678
|
+
}
|
|
2541
2679
|
function narrowingKnobsFor(spec, params) {
|
|
2542
2680
|
const properties = spec.inputSchema.properties ?? {};
|
|
2543
2681
|
const knobs = [];
|
|
@@ -2552,11 +2690,21 @@ function narrowingKnobsFor(spec, params) {
|
|
|
2552
2690
|
} else if (name === "limit") {
|
|
2553
2691
|
knobs.push({ name, kind: "limit", minimum: schema.minimum, maximum: schema.maximum, sent });
|
|
2554
2692
|
} else if (name === "selector" || name === "role" || name === "title") {
|
|
2555
|
-
|
|
2693
|
+
const target = targetSelectingKnob(spec.name, name);
|
|
2694
|
+
knobs.push(target === null ? { name, kind: "selector", sent: void 0 } : { name, kind: "target", sent: void 0, picks: target.picks, substitute: target.substitute });
|
|
2556
2695
|
}
|
|
2557
2696
|
}
|
|
2558
2697
|
return knobs;
|
|
2559
2698
|
}
|
|
2699
|
+
function replyShrinkingKnobs(knobs) {
|
|
2700
|
+
return knobs.filter((knob) => knob.kind !== "target");
|
|
2701
|
+
}
|
|
2702
|
+
function targetKnobs(knobs) {
|
|
2703
|
+
return knobs.filter((knob) => knob.kind === "target");
|
|
2704
|
+
}
|
|
2705
|
+
function describeTargetKnob(knob) {
|
|
2706
|
+
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"}`;
|
|
2707
|
+
}
|
|
2560
2708
|
function atAdvertisedFloor(knob) {
|
|
2561
2709
|
return knob.minimum !== void 0 && knob.sent !== void 0 && knob.sent <= knob.minimum;
|
|
2562
2710
|
}
|
|
@@ -2570,6 +2718,8 @@ function knobSubject(kind) {
|
|
|
2570
2718
|
return "the image";
|
|
2571
2719
|
case "selector":
|
|
2572
2720
|
return "the reply";
|
|
2721
|
+
case "target":
|
|
2722
|
+
return "the element the operation is about";
|
|
2573
2723
|
}
|
|
2574
2724
|
}
|
|
2575
2725
|
function describeKnob(knob) {
|
|
@@ -2586,16 +2736,20 @@ function describeKnob(knob) {
|
|
|
2586
2736
|
return knob.name === "selector" ? "a more specific selector (aim at one element)" : `a tighter ${knob.name} filter`;
|
|
2587
2737
|
case "scale":
|
|
2588
2738
|
return `a smaller scale${bounds}${knob.sent !== void 0 ? `, currently ${knob.sent}` : ""}`;
|
|
2739
|
+
case "target":
|
|
2740
|
+
return describeTargetKnob(knob);
|
|
2589
2741
|
}
|
|
2590
2742
|
}
|
|
2591
2743
|
function oversizedReplyAdvice(spec, params) {
|
|
2592
2744
|
const knobs = narrowingKnobsFor(spec, params);
|
|
2593
2745
|
const atFloor = knobs.filter(atAdvertisedFloor);
|
|
2594
|
-
const
|
|
2746
|
+
const target = targetKnobs(knobs);
|
|
2747
|
+
const shrinkable = replyShrinkingKnobs(knobs).filter((knob) => !atAdvertisedFloor(knob));
|
|
2595
2748
|
const head = `the reply was too large for one frame (${MAX_FRAME_BYTES}-byte limit); `;
|
|
2596
|
-
const
|
|
2749
|
+
const refused = [...atFloor.map(describeKnob), ...target.map(describeTargetKnob)];
|
|
2750
|
+
const floorNote = refused.length === 0 ? "" : `${refused.join("; ")}; `;
|
|
2597
2751
|
if (shrinkable.length === 0) {
|
|
2598
|
-
return knobs.length > 0 ? head +
|
|
2752
|
+
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";
|
|
2599
2753
|
}
|
|
2600
2754
|
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`;
|
|
2601
2755
|
}
|
|
@@ -2605,23 +2759,33 @@ var REPLAY_UNSAFE_PAYLOAD_HARMS = {
|
|
|
2605
2759
|
attach: "it re-points the daemon at another app, and a changed attach discards the evidence history a request that has not answered yet is being recorded in"
|
|
2606
2760
|
};
|
|
2607
2761
|
function replayUnsafePayloadAdvice(spec, method, params) {
|
|
2608
|
-
const
|
|
2762
|
+
const knobs = narrowingKnobsFor(spec, params);
|
|
2763
|
+
const shrinkable = replyShrinkingKnobs(knobs).filter((knob) => !atAdvertisedFloor(knob));
|
|
2764
|
+
const target = targetKnobs(knobs);
|
|
2609
2765
|
const head = `the reply was too large for one frame (${MAX_FRAME_BYTES}-byte limit); `;
|
|
2610
2766
|
const harm = REPLAY_UNSAFE_PAYLOAD_HARMS[method] ?? "it changes daemon state";
|
|
2611
|
-
const narrower = shrinkable.length > 0 ? `with ${shrinkable.map(describeKnob).join(" and ")}` : "only after narrowing it (this tool advertises no parameter that shrinks a reply, so ask a smaller question: one element, one region)";
|
|
2767
|
+
const narrower = shrinkable.length > 0 ? `with ${shrinkable.map(describeKnob).join(" and ")}` : target.length > 0 ? `exactly as it stands, with the same ${target.map((knob) => knob.name).join(" and ")} (${target.map(describeTargetKnob).join("; ")})` : "only after narrowing it (this tool advertises no parameter that shrinks a reply, so ask a smaller question: one element, one region)";
|
|
2612
2768
|
return head + `do not re-issue this request. '${spec.name}' already reached the daemon and was answered \u2014 only the answer was dropped for exceeding one frame \u2014 and ${harm}, so sending it again performs the change a second time rather than recovering the first. Read what happened instead: gp_recent_reports lists the operations this session recorded and gp_last_evidence {operationId} returns one of them in full, and gp_observe / gp_assert_element read the app the daemon is attached to now without performing anything (gp_probe_status answers on the same connection and names the apps with a live probe with their pids). The connection is intact and the daemon keeps answering other requests; if that reading shows this request never took effect, it can be sent again ${narrower}, and a retried identical request will be dropped identically.`;
|
|
2613
2769
|
}
|
|
2614
2770
|
function slowReplyAdvice(spec, params) {
|
|
2615
2771
|
const knobs = narrowingKnobsFor(spec, params);
|
|
2616
2772
|
const atFloor = knobs.filter(atAdvertisedFloor);
|
|
2617
|
-
const
|
|
2773
|
+
const target = targetKnobs(knobs);
|
|
2774
|
+
const shrinkable = replyShrinkingKnobs(knobs).filter((knob) => !atAdvertisedFloor(knob));
|
|
2618
2775
|
const floorNote = atFloor.length === 0 ? "" : ` ${atFloor.map(describeKnob).join("; ")}.`;
|
|
2619
|
-
const narrowed = shrinkable.length
|
|
2776
|
+
const narrowed = shrinkable.length > 0 ? `once it is answered, re-send '${spec.name}' with ${shrinkable.map(describeKnob).join(" and ")} \u2014 these are the parameters this tool itself advertises as deciding how much work one answer carries${floorNote}` : target.length > 0 ? `there is nothing left to narrow on this request, and no parameter of '${spec.name}' can stand in for one: ${target.map(describeTargetKnob).join("; ")}. Wait for the outstanding reply \u2014 it is attributed and recorded in this session's trail when it lands \u2014 and read the app rather than re-aiming this claim: gp_observe lists what is on screen, and gp_probe_status names the apps with a live probe` : `there is nothing left to narrow on this request:${floorNote} ask a smaller question (one element, one region) rather than re-sending the same one and waiting out the same deadline on it`;
|
|
2620
2777
|
const probeGuide = LIVENESS_PROBE_OUTCOMES.map((outcome) => `${outcome} \u2014 ${livenessProbeDecision(outcome)}`).join("; ");
|
|
2621
2778
|
return `the daemon serves one request at a time and is still working on this one, so this answer describes a busy daemon, not a dead one: wait for it, and do not put a second copy of the same request on the wire while the first is outstanding \u2014 it queues behind the work that is already late. ${narrowed}. If your client can send a second request while this one is still outstanding, confirm the daemon is alive with gp_probe_status (this shell does not queue one MCP request behind another) and read its answer as: ${probeGuide}. A restart is authorised only by ${livenessProbeDecision("unreachable")}. Nothing is discarded by this shell having answered early: ${lateReplyRoute("mcp")}`;
|
|
2622
2779
|
}
|
|
2780
|
+
var NOT_NARROWABLE_BY_RETRY = /* @__PURE__ */ new Set([
|
|
2781
|
+
...REPLAY_UNSAFE_METHOD_NAMES,
|
|
2782
|
+
...STATE_CHANGING_ON_RETRY_METHOD_NAMES
|
|
2783
|
+
]);
|
|
2784
|
+
function isNarrowableByRetryMethod(method) {
|
|
2785
|
+
return !NOT_NARROWABLE_BY_RETRY.has(method);
|
|
2786
|
+
}
|
|
2623
2787
|
function toolAuthorsTimeoutAdvice(spec, method, params) {
|
|
2624
|
-
return
|
|
2788
|
+
return isNarrowableByRetryMethod(method) && narrowingKnobsFor(spec, params).length > 0;
|
|
2625
2789
|
}
|
|
2626
2790
|
async function readTrail(session, turn, limit) {
|
|
2627
2791
|
await turn.acquire();
|
|
@@ -2672,8 +2836,12 @@ async function runValidatedTool(spec, value, engine, session, turn) {
|
|
|
2672
2836
|
} finally {
|
|
2673
2837
|
turn.release();
|
|
2674
2838
|
}
|
|
2839
|
+
const content = [{ type: "text", text: canonicalJson(raw) }];
|
|
2840
|
+
if (spec.name === "gp_diagnose") {
|
|
2841
|
+
content.push({ type: "text", text: readUpdateState().summary });
|
|
2842
|
+
}
|
|
2675
2843
|
return {
|
|
2676
|
-
content
|
|
2844
|
+
content,
|
|
2677
2845
|
isError: false
|
|
2678
2846
|
};
|
|
2679
2847
|
} catch (error) {
|
|
@@ -3157,7 +3325,7 @@ var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
|
3157
3325
|
"2025-03-26",
|
|
3158
3326
|
MCP_PROTOCOL_VERSION
|
|
3159
3327
|
];
|
|
3160
|
-
var SERVER_INFO = { name: "glasspane-mcp", version: "1.
|
|
3328
|
+
var SERVER_INFO = { name: "glasspane-mcp", version: "1.4.0" };
|
|
3161
3329
|
var PARSE_ERROR = -32700;
|
|
3162
3330
|
var INVALID_REQUEST = -32600;
|
|
3163
3331
|
var METHOD_NOT_FOUND = -32601;
|
package/dist/tools.d.ts
CHANGED
|
@@ -357,6 +357,8 @@ export declare function replayUnsafePayloadAdvice(spec: ToolSpec, method: string
|
|
|
357
357
|
* exact harm R8-高1 removed.
|
|
358
358
|
*/
|
|
359
359
|
export declare function slowReplyAdvice(spec: ToolSpec, params: Record<string, unknown>): string;
|
|
360
|
+
/** May the tool layer answer a timeout on `method` by telling the caller to re-send narrower? */
|
|
361
|
+
export declare function isNarrowableByRetryMethod(method: string): boolean;
|
|
360
362
|
/**
|
|
361
363
|
* Does the tool layer have more to say about a timeout on `method` than the
|
|
362
364
|
* transport does?
|
|
@@ -366,8 +368,9 @@ export declare function slowReplyAdvice(spec: ToolSpec, params: Record<string, u
|
|
|
366
368
|
* see the tool's schema. Everywhere else the transport's own sentence stands —
|
|
367
369
|
* including `gp_probe_status`, whose remedy the transport writes specially because
|
|
368
370
|
* a probe that went unanswered must not be told to send a probe (and must not
|
|
369
|
-
* name a restart at all).
|
|
370
|
-
*
|
|
371
|
+
* name a restart at all). And a method whose re-issue changes daemon state is not
|
|
372
|
+
* narrowed by re-sending it at all ({@link NOT_NARROWABLE_BY_RETRY}): its answer
|
|
373
|
+
* is the transport's, which orders no re-send.
|
|
371
374
|
*/
|
|
372
375
|
export declare function toolAuthorsTimeoutAdvice(spec: ToolSpec, method: string, params: Record<string, unknown>): boolean;
|
|
373
376
|
/**
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Env the daemon's own `--state-dir` maps to for this process (X-22 family). */
|
|
2
|
+
export declare const STATE_DIR_ENV = "GLASSPANE_STATE_DIR";
|
|
3
|
+
/**
|
|
4
|
+
* The home the *other* processes use. `os.homedir()` reads `$HOME`, which a caller
|
|
5
|
+
* controls; `os.userInfo().homedir` reads the password record, which is what
|
|
6
|
+
* `NSHomeDirectory()` in the panel and `StateRoot.homeDefault()` in the daemon both
|
|
7
|
+
* return. Refuse rather than fall back — the same rule `project-registry.ts` states
|
|
8
|
+
* at length, and for the same reason: a redirected `$HOME` here means reading a
|
|
9
|
+
* state file that describes nobody's machine.
|
|
10
|
+
*/
|
|
11
|
+
export declare function recordHome(): string | null;
|
|
12
|
+
/** Where `update-state.json` lives for this process, or null when it cannot be named. */
|
|
13
|
+
export declare function updateStateFile(env?: NodeJS.ProcessEnv, stateRootOverride?: string): string | null;
|
|
14
|
+
export interface UpdateStateReading {
|
|
15
|
+
kind: "read" | "absent" | "unreadable" | "unresolvable";
|
|
16
|
+
stateFile: string | null;
|
|
17
|
+
summary: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Read and render. Never throws: `gp_diagnose` answers about an operation, and a
|
|
21
|
+
* missing or broken update file is a fact about the machine, not a reason to fail
|
|
22
|
+
* the diagnosis.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readUpdateState(env?: NodeJS.ProcessEnv, stateRootOverride?: string): UpdateStateReading;
|
|
25
|
+
//# sourceMappingURL=update-state.d.ts.map
|
package/package.json
CHANGED