glasspane-mcp 1.10.0 → 1.12.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/http-gateway-cli.js +41 -15
- package/dist/http-gateway.js +41 -15
- package/dist/index.js +348 -264
- package/dist/tools.d.ts +32 -1
- package/package.json +1 -1
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.12.0";
|
|
30
30
|
};
|
|
31
31
|
export declare const PARSE_ERROR = -32700;
|
|
32
32
|
export declare const INVALID_REQUEST = -32600;
|
package/dist/http-gateway-cli.js
CHANGED
|
@@ -533,8 +533,9 @@ var EngineJsonRpcClient = class {
|
|
|
533
533
|
return;
|
|
534
534
|
}
|
|
535
535
|
if (frame.error) {
|
|
536
|
-
this.pending.delete(target.id);
|
|
537
536
|
this.reject(target, attribute(this.engineError(frame.error), target));
|
|
537
|
+
target.arrivalOrderConsumed = true;
|
|
538
|
+
this.retainForLateReply();
|
|
538
539
|
return;
|
|
539
540
|
}
|
|
540
541
|
this.report(`engine sent an id-less frame with a result while '${target.method}' (id ${target.id}) is still waiting for its answer; an id-less answer from this daemon is an error frame, so this one was not delivered to any caller: ${truncate(line)}`);
|
|
@@ -633,8 +634,9 @@ var EngineJsonRpcClient = class {
|
|
|
633
634
|
);
|
|
634
635
|
return;
|
|
635
636
|
}
|
|
637
|
+
const bodyBytes = Buffer.byteLength(jsonOrString(frame.result), "utf8");
|
|
636
638
|
this.report(
|
|
637
|
-
`late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: ${
|
|
639
|
+
`late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: the engine did answer, with a ${bodyBytes}-byte body that is raw engine content (an accessibility tree whose title/value fields are on-screen data), so its text is not written to this log${byOrder}`
|
|
638
640
|
);
|
|
639
641
|
if (frame.result === void 0) {
|
|
640
642
|
return;
|
|
@@ -817,7 +819,7 @@ var ReconnectingSocketIo = class {
|
|
|
817
819
|
const io = new StreamLineIo(socket, socket);
|
|
818
820
|
io.onMessage((line) => this.messageHandler?.(line));
|
|
819
821
|
io.onError((error) => {
|
|
820
|
-
if (this.inner === io) {
|
|
822
|
+
if (!(error instanceof OversizeFrameError) && this.inner === io) {
|
|
821
823
|
this.inner = null;
|
|
822
824
|
io.close();
|
|
823
825
|
}
|
|
@@ -1885,8 +1887,8 @@ var ProjectRegistryError = class extends Error {
|
|
|
1885
1887
|
var PID_INT32_MAX = 2147483647;
|
|
1886
1888
|
var OptionalPidSchema = z5.number().int().min(1).max(PID_INT32_MAX).optional();
|
|
1887
1889
|
var OptionalDepthSchema = z5.number().int().min(1).max(10).optional();
|
|
1888
|
-
var OptionalRoleSchema = z5.string().max(
|
|
1889
|
-
var OptionalBundleIdSchema = z5.string().max(
|
|
1890
|
+
var OptionalRoleSchema = z5.string().max(SELECTOR_MAX_LENGTH).optional();
|
|
1891
|
+
var OptionalBundleIdSchema = z5.string().max(512).optional();
|
|
1890
1892
|
var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
1891
1893
|
var OptionalOperationIdSchema = z5.string().regex(OPERATION_ID_PATTERN2).optional();
|
|
1892
1894
|
var OptionalProjectIdSchema = z5.string().regex(/^prj_[0-9A-HJKMNP-TV-Z]{26}$/).optional();
|
|
@@ -2002,7 +2004,7 @@ var TOOL_SPECS = [
|
|
|
2002
2004
|
type: "object",
|
|
2003
2005
|
additionalProperties: false,
|
|
2004
2006
|
properties: {
|
|
2005
|
-
bundleId: { type: "string", maxLength:
|
|
2007
|
+
bundleId: { type: "string", maxLength: 512 },
|
|
2006
2008
|
pid: { type: "integer", minimum: 1, maximum: PID_INT32_MAX },
|
|
2007
2009
|
projectId: { type: "string", pattern: "^prj_[0-9A-HJKMNP-TV-Z]{26}$" }
|
|
2008
2010
|
},
|
|
@@ -2022,7 +2024,7 @@ var TOOL_SPECS = [
|
|
|
2022
2024
|
additionalProperties: false,
|
|
2023
2025
|
properties: {
|
|
2024
2026
|
maxDepth: { type: "integer", minimum: 1, maximum: 10 },
|
|
2025
|
-
role: { type: "string", maxLength:
|
|
2027
|
+
role: { type: "string", maxLength: SELECTOR_MAX_LENGTH }
|
|
2026
2028
|
}
|
|
2027
2029
|
},
|
|
2028
2030
|
validate: zodBridge(ObserveArgs)
|
|
@@ -2370,6 +2372,9 @@ var NOT_NARROWABLE_BY_RETRY = /* @__PURE__ */ new Set([
|
|
|
2370
2372
|
...REPLAY_UNSAFE_METHOD_NAMES,
|
|
2371
2373
|
...STATE_CHANGING_ON_RETRY_METHOD_NAMES
|
|
2372
2374
|
]);
|
|
2375
|
+
function isNarrowableByRetryMethod(method) {
|
|
2376
|
+
return !NOT_NARROWABLE_BY_RETRY.has(method);
|
|
2377
|
+
}
|
|
2373
2378
|
async function readTrail(session, turn, limit) {
|
|
2374
2379
|
await turn.acquire();
|
|
2375
2380
|
try {
|
|
@@ -2378,6 +2383,17 @@ async function readTrail(session, turn, limit) {
|
|
|
2378
2383
|
turn.release();
|
|
2379
2384
|
}
|
|
2380
2385
|
}
|
|
2386
|
+
function shellFaultRemedy(engineMethod) {
|
|
2387
|
+
const readRoute = "read what happened instead of performing it again: gp_recent_reports lists the operations this session recorded, gp_last_evidence {operationId} returns one in full, and gp_probe_status reads the daemon's own self-report on this same connection \u2014 all three are requests this shell does not treat as unsafe to repeat";
|
|
2388
|
+
const reportRoute = "this fault was raised inside the MCP shell, so it is in no daemon log: this process's stderr belongs to whoever started this server, and that line is the only text naming the failure, so hand it to them verbatim. A daemon restart is not authorised by an answer the daemon never made";
|
|
2389
|
+
if (isReplayUnsafeMethod(engineMethod)) {
|
|
2390
|
+
return `this is a defect in this shell, not an answer from the engine, so it measures nothing about whether '${engineMethod}' reached the daemon or the user's screen. Do NOT re-issue ${engineMethod}: a re-issue can perform the same action a second time on the user's screen (and for attach, re-point the daemon and discard the trail an outstanding request is being recorded in), and this failure does not establish that the first copy did nothing. ${readRoute}. ${reportRoute}`;
|
|
2391
|
+
}
|
|
2392
|
+
if (!isNarrowableByRetryMethod(engineMethod)) {
|
|
2393
|
+
return `this is a defect in this shell, not an answer from the engine. '${engineMethod}' is a method whose re-issue writes daemon state \u2014 a fresh request appends to the bounded snapshot ring and can evict a baseline this caller already received \u2014 so treat it as a question to read around rather than one to send again: gp_snapshot lists stored ids, and ${readRoute}. ${reportRoute}`;
|
|
2394
|
+
}
|
|
2395
|
+
return `this is a defect in this shell, not an answer from the engine. '${engineMethod}' is not one of the methods this shell classifies as unsafe to repeat (the no-replay set is act/restore/attach, plus snapshot, whose re-issue writes daemon state), so sending this tool again is a real option rather than a side effect: do that once. If the fault repeats, ${readRoute}. ${reportRoute}`;
|
|
2396
|
+
}
|
|
2381
2397
|
var EvidenceFrameShapeError = class extends Error {
|
|
2382
2398
|
constructor(detail) {
|
|
2383
2399
|
super(`engine returned a last_evidence frame without an evidencePack object: ${detail}`);
|
|
@@ -2433,7 +2449,17 @@ async function captureView(args, context) {
|
|
|
2433
2449
|
isError: true
|
|
2434
2450
|
};
|
|
2435
2451
|
}
|
|
2436
|
-
|
|
2452
|
+
if (typeof body.mimeType !== "string" || body.mimeType.length === 0) {
|
|
2453
|
+
return {
|
|
2454
|
+
content: [{ type: "text", text: formatToolError(
|
|
2455
|
+
GP_E_INTERNAL,
|
|
2456
|
+
`capture_view answered without a mimeType (got ${JSON.stringify(body.mimeType ?? null)})`,
|
|
2457
|
+
"do not read this image as any particular format: the engine did not name one, and this shell will not fill the field in. This is a read-only capture, so sending it again performs nothing \u2014 if it repeats, report the frame shape, which is a protocol disagreement between two builds rather than a dead daemon"
|
|
2458
|
+
) }],
|
|
2459
|
+
isError: true
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
const mimeType = body.mimeType;
|
|
2437
2463
|
const summary = canonicalJson2({
|
|
2438
2464
|
mimeType,
|
|
2439
2465
|
byteCount: body.byteCount,
|
|
@@ -2465,7 +2491,7 @@ async function captureView(args, context) {
|
|
|
2465
2491
|
content: [{ type: "text", text: formatToolError(
|
|
2466
2492
|
GP_E_INTERNAL,
|
|
2467
2493
|
`internal shell error: ${String(error)}`,
|
|
2468
|
-
"
|
|
2494
|
+
shellFaultRemedy("capture_view")
|
|
2469
2495
|
) }],
|
|
2470
2496
|
isError: true
|
|
2471
2497
|
};
|
|
@@ -2566,7 +2592,7 @@ function projectErrorRemedy(code) {
|
|
|
2566
2592
|
return "this process has no entry in the password database, so neither it nor the daemon can know which user's state directory is meant; run the shell as a user that has one (`id -u` / `dscl . -read /Users/<name> NFSHomeDirectory`). Nothing in ~/.glasspane needs repairing for this";
|
|
2567
2593
|
}
|
|
2568
2594
|
if (code === GP_E_INTERNAL) {
|
|
2569
|
-
return `the projects file is the problem, not your arguments: the message above names its path, so read it with
|
|
2595
|
+
return `the projects file is the problem, not your arguments: the message above names its path, so read it with \`/usr/bin/plutil -p <that path>\` or \`/bin/cat <that path>\` \u2014 both absolute, so they run from whatever directory the reader is in and need nothing that may be off PATH \u2014 repair or restore the damaged entry, and retry \u2014 or set ${FORCE_OVERWRITE_ENV}=1 and call gp_project_set to rebuild the registry from scratch (the unloadable file is moved aside as <path>.unreadable-<id>, never deleted, and every entry still in it is lost). Do not retry the same read-only call unchanged: it will keep failing.`;
|
|
2570
2596
|
}
|
|
2571
2597
|
return "check the tool's input schema and retry";
|
|
2572
2598
|
}
|
|
@@ -2641,7 +2667,7 @@ function renderTrailIds(ids, format) {
|
|
|
2641
2667
|
}
|
|
2642
2668
|
function describeFetchFailure(error) {
|
|
2643
2669
|
if (error instanceof EngineCallError) {
|
|
2644
|
-
return
|
|
2670
|
+
return formatToolErrorShape(error.toBody());
|
|
2645
2671
|
}
|
|
2646
2672
|
const mapped = mapEvidenceReadError(error);
|
|
2647
2673
|
if (mapped !== void 0) {
|
|
@@ -2762,8 +2788,8 @@ async function recentReports(args, context) {
|
|
|
2762
2788
|
return {
|
|
2763
2789
|
content: [{ type: "text", text: formatToolError(
|
|
2764
2790
|
GP_E_NO_EVIDENCE,
|
|
2765
|
-
|
|
2766
|
-
"the
|
|
2791
|
+
`no pack is reachable in the daemon history for any of the ${ids.length} ${ids.length === 1 ? "operation" : "operations"} this shell asked about: each was answered GP_E_NO_EVIDENCE (${renderTrailIds(skipped, format)})`,
|
|
2792
|
+
"these operations are recorded in this session's trail, and the daemon holding no pack for them is a fact about its bounded history, not evidence that they did not run \u2014 this call measured nothing about a restart, so no cause is claimed here. Read one of them by id with gp_last_evidence {operationId} / gp_export_evidence {operationId}, or ask again with a smaller limit (gp_recent_reports advertises 1\u202620). Do not re-run gp_act to regenerate what the trail already holds"
|
|
2767
2793
|
) }],
|
|
2768
2794
|
isError: true
|
|
2769
2795
|
};
|
|
@@ -2786,7 +2812,7 @@ async function recentReports(args, context) {
|
|
|
2786
2812
|
sections2.push(body);
|
|
2787
2813
|
return { content: [{ type: "text", text: sections2.join("\n") }], isError: false };
|
|
2788
2814
|
}
|
|
2789
|
-
function mapAuditError(error) {
|
|
2815
|
+
function mapAuditError(error, engineMethod = "last_evidence") {
|
|
2790
2816
|
if (error instanceof EngineCallError) {
|
|
2791
2817
|
return {
|
|
2792
2818
|
content: [{ type: "text", text: formatToolErrorShape(error.toBody()) }],
|
|
@@ -2801,7 +2827,7 @@ function mapAuditError(error) {
|
|
|
2801
2827
|
content: [{ type: "text", text: formatToolError(
|
|
2802
2828
|
GP_E_INTERNAL,
|
|
2803
2829
|
`internal shell error: ${String(error)}`,
|
|
2804
|
-
|
|
2830
|
+
shellFaultRemedy(engineMethod)
|
|
2805
2831
|
) }],
|
|
2806
2832
|
isError: true
|
|
2807
2833
|
};
|
package/dist/http-gateway.js
CHANGED
|
@@ -529,8 +529,9 @@ var EngineJsonRpcClient = class {
|
|
|
529
529
|
return;
|
|
530
530
|
}
|
|
531
531
|
if (frame.error) {
|
|
532
|
-
this.pending.delete(target.id);
|
|
533
532
|
this.reject(target, attribute(this.engineError(frame.error), target));
|
|
533
|
+
target.arrivalOrderConsumed = true;
|
|
534
|
+
this.retainForLateReply();
|
|
534
535
|
return;
|
|
535
536
|
}
|
|
536
537
|
this.report(`engine sent an id-less frame with a result while '${target.method}' (id ${target.id}) is still waiting for its answer; an id-less answer from this daemon is an error frame, so this one was not delivered to any caller: ${truncate(line)}`);
|
|
@@ -629,8 +630,9 @@ var EngineJsonRpcClient = class {
|
|
|
629
630
|
);
|
|
630
631
|
return;
|
|
631
632
|
}
|
|
633
|
+
const bodyBytes = Buffer.byteLength(jsonOrString(frame.result), "utf8");
|
|
632
634
|
this.report(
|
|
633
|
-
`late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: ${
|
|
635
|
+
`late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: the engine did answer, with a ${bodyBytes}-byte body that is raw engine content (an accessibility tree whose title/value fields are on-screen data), so its text is not written to this log${byOrder}`
|
|
634
636
|
);
|
|
635
637
|
if (frame.result === void 0) {
|
|
636
638
|
return;
|
|
@@ -813,7 +815,7 @@ var ReconnectingSocketIo = class {
|
|
|
813
815
|
const io = new StreamLineIo(socket, socket);
|
|
814
816
|
io.onMessage((line) => this.messageHandler?.(line));
|
|
815
817
|
io.onError((error) => {
|
|
816
|
-
if (this.inner === io) {
|
|
818
|
+
if (!(error instanceof OversizeFrameError) && this.inner === io) {
|
|
817
819
|
this.inner = null;
|
|
818
820
|
io.close();
|
|
819
821
|
}
|
|
@@ -1875,8 +1877,8 @@ var ProjectRegistryError = class extends Error {
|
|
|
1875
1877
|
var PID_INT32_MAX = 2147483647;
|
|
1876
1878
|
var OptionalPidSchema = z5.number().int().min(1).max(PID_INT32_MAX).optional();
|
|
1877
1879
|
var OptionalDepthSchema = z5.number().int().min(1).max(10).optional();
|
|
1878
|
-
var OptionalRoleSchema = z5.string().max(
|
|
1879
|
-
var OptionalBundleIdSchema = z5.string().max(
|
|
1880
|
+
var OptionalRoleSchema = z5.string().max(SELECTOR_MAX_LENGTH).optional();
|
|
1881
|
+
var OptionalBundleIdSchema = z5.string().max(512).optional();
|
|
1880
1882
|
var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
1881
1883
|
var OptionalOperationIdSchema = z5.string().regex(OPERATION_ID_PATTERN2).optional();
|
|
1882
1884
|
var OptionalProjectIdSchema = z5.string().regex(/^prj_[0-9A-HJKMNP-TV-Z]{26}$/).optional();
|
|
@@ -1992,7 +1994,7 @@ var TOOL_SPECS = [
|
|
|
1992
1994
|
type: "object",
|
|
1993
1995
|
additionalProperties: false,
|
|
1994
1996
|
properties: {
|
|
1995
|
-
bundleId: { type: "string", maxLength:
|
|
1997
|
+
bundleId: { type: "string", maxLength: 512 },
|
|
1996
1998
|
pid: { type: "integer", minimum: 1, maximum: PID_INT32_MAX },
|
|
1997
1999
|
projectId: { type: "string", pattern: "^prj_[0-9A-HJKMNP-TV-Z]{26}$" }
|
|
1998
2000
|
},
|
|
@@ -2012,7 +2014,7 @@ var TOOL_SPECS = [
|
|
|
2012
2014
|
additionalProperties: false,
|
|
2013
2015
|
properties: {
|
|
2014
2016
|
maxDepth: { type: "integer", minimum: 1, maximum: 10 },
|
|
2015
|
-
role: { type: "string", maxLength:
|
|
2017
|
+
role: { type: "string", maxLength: SELECTOR_MAX_LENGTH }
|
|
2016
2018
|
}
|
|
2017
2019
|
},
|
|
2018
2020
|
validate: zodBridge(ObserveArgs)
|
|
@@ -2360,6 +2362,9 @@ var NOT_NARROWABLE_BY_RETRY = /* @__PURE__ */ new Set([
|
|
|
2360
2362
|
...REPLAY_UNSAFE_METHOD_NAMES,
|
|
2361
2363
|
...STATE_CHANGING_ON_RETRY_METHOD_NAMES
|
|
2362
2364
|
]);
|
|
2365
|
+
function isNarrowableByRetryMethod(method) {
|
|
2366
|
+
return !NOT_NARROWABLE_BY_RETRY.has(method);
|
|
2367
|
+
}
|
|
2363
2368
|
async function readTrail(session, turn, limit) {
|
|
2364
2369
|
await turn.acquire();
|
|
2365
2370
|
try {
|
|
@@ -2368,6 +2373,17 @@ async function readTrail(session, turn, limit) {
|
|
|
2368
2373
|
turn.release();
|
|
2369
2374
|
}
|
|
2370
2375
|
}
|
|
2376
|
+
function shellFaultRemedy(engineMethod) {
|
|
2377
|
+
const readRoute = "read what happened instead of performing it again: gp_recent_reports lists the operations this session recorded, gp_last_evidence {operationId} returns one in full, and gp_probe_status reads the daemon's own self-report on this same connection \u2014 all three are requests this shell does not treat as unsafe to repeat";
|
|
2378
|
+
const reportRoute = "this fault was raised inside the MCP shell, so it is in no daemon log: this process's stderr belongs to whoever started this server, and that line is the only text naming the failure, so hand it to them verbatim. A daemon restart is not authorised by an answer the daemon never made";
|
|
2379
|
+
if (isReplayUnsafeMethod(engineMethod)) {
|
|
2380
|
+
return `this is a defect in this shell, not an answer from the engine, so it measures nothing about whether '${engineMethod}' reached the daemon or the user's screen. Do NOT re-issue ${engineMethod}: a re-issue can perform the same action a second time on the user's screen (and for attach, re-point the daemon and discard the trail an outstanding request is being recorded in), and this failure does not establish that the first copy did nothing. ${readRoute}. ${reportRoute}`;
|
|
2381
|
+
}
|
|
2382
|
+
if (!isNarrowableByRetryMethod(engineMethod)) {
|
|
2383
|
+
return `this is a defect in this shell, not an answer from the engine. '${engineMethod}' is a method whose re-issue writes daemon state \u2014 a fresh request appends to the bounded snapshot ring and can evict a baseline this caller already received \u2014 so treat it as a question to read around rather than one to send again: gp_snapshot lists stored ids, and ${readRoute}. ${reportRoute}`;
|
|
2384
|
+
}
|
|
2385
|
+
return `this is a defect in this shell, not an answer from the engine. '${engineMethod}' is not one of the methods this shell classifies as unsafe to repeat (the no-replay set is act/restore/attach, plus snapshot, whose re-issue writes daemon state), so sending this tool again is a real option rather than a side effect: do that once. If the fault repeats, ${readRoute}. ${reportRoute}`;
|
|
2386
|
+
}
|
|
2371
2387
|
var EvidenceFrameShapeError = class extends Error {
|
|
2372
2388
|
constructor(detail) {
|
|
2373
2389
|
super(`engine returned a last_evidence frame without an evidencePack object: ${detail}`);
|
|
@@ -2423,7 +2439,17 @@ async function captureView(args, context) {
|
|
|
2423
2439
|
isError: true
|
|
2424
2440
|
};
|
|
2425
2441
|
}
|
|
2426
|
-
|
|
2442
|
+
if (typeof body.mimeType !== "string" || body.mimeType.length === 0) {
|
|
2443
|
+
return {
|
|
2444
|
+
content: [{ type: "text", text: formatToolError(
|
|
2445
|
+
GP_E_INTERNAL,
|
|
2446
|
+
`capture_view answered without a mimeType (got ${JSON.stringify(body.mimeType ?? null)})`,
|
|
2447
|
+
"do not read this image as any particular format: the engine did not name one, and this shell will not fill the field in. This is a read-only capture, so sending it again performs nothing \u2014 if it repeats, report the frame shape, which is a protocol disagreement between two builds rather than a dead daemon"
|
|
2448
|
+
) }],
|
|
2449
|
+
isError: true
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
const mimeType = body.mimeType;
|
|
2427
2453
|
const summary = canonicalJson2({
|
|
2428
2454
|
mimeType,
|
|
2429
2455
|
byteCount: body.byteCount,
|
|
@@ -2455,7 +2481,7 @@ async function captureView(args, context) {
|
|
|
2455
2481
|
content: [{ type: "text", text: formatToolError(
|
|
2456
2482
|
GP_E_INTERNAL,
|
|
2457
2483
|
`internal shell error: ${String(error)}`,
|
|
2458
|
-
"
|
|
2484
|
+
shellFaultRemedy("capture_view")
|
|
2459
2485
|
) }],
|
|
2460
2486
|
isError: true
|
|
2461
2487
|
};
|
|
@@ -2556,7 +2582,7 @@ function projectErrorRemedy(code) {
|
|
|
2556
2582
|
return "this process has no entry in the password database, so neither it nor the daemon can know which user's state directory is meant; run the shell as a user that has one (`id -u` / `dscl . -read /Users/<name> NFSHomeDirectory`). Nothing in ~/.glasspane needs repairing for this";
|
|
2557
2583
|
}
|
|
2558
2584
|
if (code === GP_E_INTERNAL) {
|
|
2559
|
-
return `the projects file is the problem, not your arguments: the message above names its path, so read it with
|
|
2585
|
+
return `the projects file is the problem, not your arguments: the message above names its path, so read it with \`/usr/bin/plutil -p <that path>\` or \`/bin/cat <that path>\` \u2014 both absolute, so they run from whatever directory the reader is in and need nothing that may be off PATH \u2014 repair or restore the damaged entry, and retry \u2014 or set ${FORCE_OVERWRITE_ENV}=1 and call gp_project_set to rebuild the registry from scratch (the unloadable file is moved aside as <path>.unreadable-<id>, never deleted, and every entry still in it is lost). Do not retry the same read-only call unchanged: it will keep failing.`;
|
|
2560
2586
|
}
|
|
2561
2587
|
return "check the tool's input schema and retry";
|
|
2562
2588
|
}
|
|
@@ -2631,7 +2657,7 @@ function renderTrailIds(ids, format) {
|
|
|
2631
2657
|
}
|
|
2632
2658
|
function describeFetchFailure(error) {
|
|
2633
2659
|
if (error instanceof EngineCallError) {
|
|
2634
|
-
return
|
|
2660
|
+
return formatToolErrorShape(error.toBody());
|
|
2635
2661
|
}
|
|
2636
2662
|
const mapped = mapEvidenceReadError(error);
|
|
2637
2663
|
if (mapped !== void 0) {
|
|
@@ -2752,8 +2778,8 @@ async function recentReports(args, context) {
|
|
|
2752
2778
|
return {
|
|
2753
2779
|
content: [{ type: "text", text: formatToolError(
|
|
2754
2780
|
GP_E_NO_EVIDENCE,
|
|
2755
|
-
|
|
2756
|
-
"the
|
|
2781
|
+
`no pack is reachable in the daemon history for any of the ${ids.length} ${ids.length === 1 ? "operation" : "operations"} this shell asked about: each was answered GP_E_NO_EVIDENCE (${renderTrailIds(skipped, format)})`,
|
|
2782
|
+
"these operations are recorded in this session's trail, and the daemon holding no pack for them is a fact about its bounded history, not evidence that they did not run \u2014 this call measured nothing about a restart, so no cause is claimed here. Read one of them by id with gp_last_evidence {operationId} / gp_export_evidence {operationId}, or ask again with a smaller limit (gp_recent_reports advertises 1\u202620). Do not re-run gp_act to regenerate what the trail already holds"
|
|
2757
2783
|
) }],
|
|
2758
2784
|
isError: true
|
|
2759
2785
|
};
|
|
@@ -2776,7 +2802,7 @@ async function recentReports(args, context) {
|
|
|
2776
2802
|
sections2.push(body);
|
|
2777
2803
|
return { content: [{ type: "text", text: sections2.join("\n") }], isError: false };
|
|
2778
2804
|
}
|
|
2779
|
-
function mapAuditError(error) {
|
|
2805
|
+
function mapAuditError(error, engineMethod = "last_evidence") {
|
|
2780
2806
|
if (error instanceof EngineCallError) {
|
|
2781
2807
|
return {
|
|
2782
2808
|
content: [{ type: "text", text: formatToolErrorShape(error.toBody()) }],
|
|
@@ -2791,7 +2817,7 @@ function mapAuditError(error) {
|
|
|
2791
2817
|
content: [{ type: "text", text: formatToolError(
|
|
2792
2818
|
GP_E_INTERNAL,
|
|
2793
2819
|
`internal shell error: ${String(error)}`,
|
|
2794
|
-
|
|
2820
|
+
shellFaultRemedy(engineMethod)
|
|
2795
2821
|
) }],
|
|
2796
2822
|
isError: true
|
|
2797
2823
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import fs5 from "node:fs";
|
|
4
5
|
import process2 from "node:process";
|
|
5
6
|
|
|
6
7
|
// src/canonical.ts
|
|
@@ -24,6 +25,9 @@ function sortKeys(value) {
|
|
|
24
25
|
|
|
25
26
|
// src/tools.ts
|
|
26
27
|
import { z as z5 } from "zod";
|
|
28
|
+
import fs4 from "node:fs";
|
|
29
|
+
import path4 from "node:path";
|
|
30
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
27
31
|
|
|
28
32
|
// ../kernel/dist/schemas.js
|
|
29
33
|
import { createRequire } from "node:module";
|
|
@@ -1189,8 +1193,9 @@ var EngineJsonRpcClient = class {
|
|
|
1189
1193
|
return;
|
|
1190
1194
|
}
|
|
1191
1195
|
if (frame.error) {
|
|
1192
|
-
this.pending.delete(target.id);
|
|
1193
1196
|
this.reject(target, attribute(this.engineError(frame.error), target));
|
|
1197
|
+
target.arrivalOrderConsumed = true;
|
|
1198
|
+
this.retainForLateReply();
|
|
1194
1199
|
return;
|
|
1195
1200
|
}
|
|
1196
1201
|
this.report(`engine sent an id-less frame with a result while '${target.method}' (id ${target.id}) is still waiting for its answer; an id-less answer from this daemon is an error frame, so this one was not delivered to any caller: ${truncate(line)}`);
|
|
@@ -1289,8 +1294,9 @@ var EngineJsonRpcClient = class {
|
|
|
1289
1294
|
);
|
|
1290
1295
|
return;
|
|
1291
1296
|
}
|
|
1297
|
+
const bodyBytes = Buffer.byteLength(jsonOrString(frame.result), "utf8");
|
|
1292
1298
|
this.report(
|
|
1293
|
-
`late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: ${
|
|
1299
|
+
`late engine reply for '${entry.method}' (id ${entry.id}), ${entry.deadlineMs}ms deadline already reported: the engine did answer, with a ${bodyBytes}-byte body that is raw engine content (an accessibility tree whose title/value fields are on-screen data), so its text is not written to this log${byOrder}`
|
|
1294
1300
|
);
|
|
1295
1301
|
if (frame.result === void 0) {
|
|
1296
1302
|
return;
|
|
@@ -1473,7 +1479,7 @@ var ReconnectingSocketIo = class {
|
|
|
1473
1479
|
const io = new StreamLineIo(socket, socket);
|
|
1474
1480
|
io.onMessage((line) => this.messageHandler?.(line));
|
|
1475
1481
|
io.onError((error) => {
|
|
1476
|
-
if (this.inner === io) {
|
|
1482
|
+
if (!(error instanceof OversizeFrameError) && this.inner === io) {
|
|
1477
1483
|
this.inner = null;
|
|
1478
1484
|
io.close();
|
|
1479
1485
|
}
|
|
@@ -1505,243 +1511,6 @@ function unixSocketEngineClient(socketPath, options = {}) {
|
|
|
1505
1511
|
);
|
|
1506
1512
|
}
|
|
1507
1513
|
|
|
1508
|
-
// src/audit-session.ts
|
|
1509
|
-
var AUDIT_HISTORY_LIMIT = 20;
|
|
1510
|
-
var UNKNOWN_GENERATION = -1;
|
|
1511
|
-
function openTrailGate() {
|
|
1512
|
-
let settle;
|
|
1513
|
-
const settled = new Promise((resolve) => {
|
|
1514
|
-
settle = () => resolve(void 0);
|
|
1515
|
-
});
|
|
1516
|
-
return { settled, settle };
|
|
1517
|
-
}
|
|
1518
|
-
var EvidenceAuditSession = class {
|
|
1519
|
-
ids = [];
|
|
1520
|
-
epoch = 0;
|
|
1521
|
-
attachedAppKey = null;
|
|
1522
|
-
/**
|
|
1523
|
-
* Turns claimed but not released. This, not a "latest gate", is what a new
|
|
1524
|
-
* claim waits for: a turn that abandons the trail (its engine call failed
|
|
1525
|
-
* before any mutation) releases *out of order*, and a chain that only looked
|
|
1526
|
-
* at the immediately preceding turn would let everything behind it run while
|
|
1527
|
-
* an older, still-outstanding act had not written yet — the exact completion
|
|
1528
|
-
* order the turn machinery exists to remove.
|
|
1529
|
-
*/
|
|
1530
|
-
openGates = /* @__PURE__ */ new Set();
|
|
1531
|
-
/**
|
|
1532
|
-
* How each id in {@link ids} got here. Keyed by the id, because that is what a
|
|
1533
|
-
* reader holds; the map is trimmed with the list and cleared when the trail
|
|
1534
|
-
* restarts, so an id can never outlive the provenance that describes it.
|
|
1535
|
-
*/
|
|
1536
|
-
provenance = /* @__PURE__ */ new Map();
|
|
1537
|
-
/**
|
|
1538
|
-
* Non-null while {@link recordLateArrival} holds the trail, and says how the
|
|
1539
|
-
* frame being written was attributed. This is what lets a plain
|
|
1540
|
-
* `trail.record(result)` inside that callback be filed as a late arrival without
|
|
1541
|
-
* the caller having to remember to say so: the write happens inside a window only
|
|
1542
|
-
* that method opens, so the mechanism is visible here rather than being a claim
|
|
1543
|
-
* somebody must repeat at every call site.
|
|
1544
|
-
*/
|
|
1545
|
-
lateWindow = null;
|
|
1546
|
-
/**
|
|
1547
|
-
* Record every operationId/evidenceId found in an engine result frame, tagged
|
|
1548
|
-
* with the provenance of the path that is recording it (see
|
|
1549
|
-
* {@link TrailProvenance}): `in-order` from a call that held its own trail turn,
|
|
1550
|
-
* `late`/`late-inferred` from inside {@link recordLateArrival}.
|
|
1551
|
-
*/
|
|
1552
|
-
record(result) {
|
|
1553
|
-
const source = this.lateWindow === null ? "in-order" : this.lateWindow === "arrival-order" ? "late-inferred" : "late";
|
|
1554
|
-
for (const id of collectOperationIds(result)) {
|
|
1555
|
-
if (!this.ids.includes(id)) {
|
|
1556
|
-
this.ids.push(id);
|
|
1557
|
-
this.provenance.set(id, source);
|
|
1558
|
-
if (this.ids.length > AUDIT_HISTORY_LIMIT) {
|
|
1559
|
-
const evicted = this.ids.splice(0, this.ids.length - AUDIT_HISTORY_LIMIT);
|
|
1560
|
-
for (const gone of evicted) {
|
|
1561
|
-
this.provenance.delete(gone);
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
/**
|
|
1568
|
-
* How one id in this trail got recorded, or null when the trail does not hold
|
|
1569
|
-
* it — never recorded, evicted past {@link AUDIT_HISTORY_LIMIT}, or cleared by a
|
|
1570
|
-
* {@link restart}. A reader that lists ids for an agent renders
|
|
1571
|
-
* {@link TRAIL_PROVENANCE_NOTES} for anything that is not `in-order`: an id
|
|
1572
|
-
* pulled from a frame this shell only *guessed* an owner for must not appear
|
|
1573
|
-
* beside one it matched by id (R10-中2).
|
|
1574
|
-
*
|
|
1575
|
-
* Read off the map alone, so the map *is* the record of what the trail holds: an
|
|
1576
|
-
* id whose provenance outlives its own entry would come back labelled as if it
|
|
1577
|
-
* were still listed, and the eviction and the restart are the two places that
|
|
1578
|
-
* would have to remember to delete. `test/engine-client.test.mjs` pins both.
|
|
1579
|
-
*/
|
|
1580
|
-
provenanceOf(id) {
|
|
1581
|
-
return this.provenance.get(id) ?? null;
|
|
1582
|
-
}
|
|
1583
|
-
/** The ids currently filed as inferred; see {@link TrailProvenance}. */
|
|
1584
|
-
inferredIds() {
|
|
1585
|
-
return this.ids.filter((id) => this.provenance.get(id) === "late-inferred");
|
|
1586
|
-
}
|
|
1587
|
-
/**
|
|
1588
|
-
* The trail's generation number. Every {@link restart} bumps it.
|
|
1589
|
-
*
|
|
1590
|
-
* It exists so a reply that arrives *after* its caller was already answered
|
|
1591
|
-
* can be judged against the trail it was written into: {@link recordLateArrival}
|
|
1592
|
-
* takes the epoch the request was admitted under, and a mismatch means a
|
|
1593
|
-
* re-attach happened in between. Without it, a late write lands in a *new*
|
|
1594
|
-
* session's trail — an operationId from the previous app that
|
|
1595
|
-
* `gp_recent_reports` then fetches and renders as if it belonged to this one
|
|
1596
|
-
* (R8b-高1, measured: `['op_OLDAPP']` in the trail after attaching a second
|
|
1597
|
-
* app).
|
|
1598
|
-
*/
|
|
1599
|
-
get generation() {
|
|
1600
|
-
return this.epoch;
|
|
1601
|
-
}
|
|
1602
|
-
/** The app the trail currently belongs to, or null before the first attach. */
|
|
1603
|
-
get attachedApp() {
|
|
1604
|
-
return this.attachedAppKey;
|
|
1605
|
-
}
|
|
1606
|
-
/**
|
|
1607
|
-
* A successful attach: the trail restarts **only when the attached app
|
|
1608
|
-
* changed**, because that is the daemon's own rule —
|
|
1609
|
-
* `EngineCore.attach` clears `history` under
|
|
1610
|
-
* `if attachedApp != app`, and its comment says re-attach to the same app is
|
|
1611
|
-
* idempotent. Clearing on *every* attach (what this used to do) is strictly
|
|
1612
|
-
* worse in two directions: an idempotent re-attach wiped operations that were
|
|
1613
|
-
* really in the trail (including one recorded by a late reply, which put
|
|
1614
|
-
* `gp_recent_reports` back to "run gp_act first"), and — because the clear
|
|
1615
|
-
* happens whenever the reply lands — it could also drop the record of an act
|
|
1616
|
-
* the daemon still has.
|
|
1617
|
-
*
|
|
1618
|
-
* `appKey` must be built from every stored property of Swift's `AttachedApp`
|
|
1619
|
-
* (`pid`, `bundleId`, `appName`); a key that ignored `pid` would keep the
|
|
1620
|
-
* trail across an app restart whose history the daemon did clear. The gate
|
|
1621
|
-
* that pins this to the Swift struct is
|
|
1622
|
-
* `test/attach-identity.test.mjs`.
|
|
1623
|
-
*
|
|
1624
|
-
* Returns whether the trail was restarted. A null `appKey` means the attach
|
|
1625
|
-
* result did not identify the app at all, which is treated as a change: an
|
|
1626
|
-
* unknown identity cannot be *shown* to be the same app, and a stale trail is
|
|
1627
|
-
* the failure that misattributes evidence.
|
|
1628
|
-
*/
|
|
1629
|
-
restart(appKey) {
|
|
1630
|
-
if (appKey !== null && appKey === this.attachedAppKey) {
|
|
1631
|
-
return false;
|
|
1632
|
-
}
|
|
1633
|
-
this.attachedAppKey = appKey;
|
|
1634
|
-
this.ids = [];
|
|
1635
|
-
this.provenance.clear();
|
|
1636
|
-
this.epoch += 1;
|
|
1637
|
-
return true;
|
|
1638
|
-
}
|
|
1639
|
-
/** The most recent `limit` recorded operationIds, oldest first. */
|
|
1640
|
-
recentIds(limit) {
|
|
1641
|
-
return this.ids.slice(-limit);
|
|
1642
|
-
}
|
|
1643
|
-
/**
|
|
1644
|
-
* Record a frame that arrives with **no request of the caller still holding a
|
|
1645
|
-
* turn**: a late engine reply (R8-高3, `EngineJsonRpcClient.onLateReply`). Its
|
|
1646
|
-
* own turn was released the moment the timeout was reported to the agent, so
|
|
1647
|
-
* there is no request-order position left to write at.
|
|
1648
|
-
*
|
|
1649
|
-
* It still goes through the turn machinery rather than a bare `record()`,
|
|
1650
|
-
* because a direct write could land *between* two admitted calls and reorder
|
|
1651
|
-
* the trail against the requests the agent made — the exact thing the turns
|
|
1652
|
-
* exist to prevent. Claiming on arrival gives the only honest position left:
|
|
1653
|
-
* behind whatever is still outstanding, and ordered among late arrivals by
|
|
1654
|
-
* when they arrived.
|
|
1655
|
-
*
|
|
1656
|
-
* `admittedUnderGeneration` is compared **inside** that wait (see the body):
|
|
1657
|
-
* the position and the epoch can only be judged at the moment the write
|
|
1658
|
-
* actually happens. A reply whose request carried no generation cannot be
|
|
1659
|
-
* placed either — so it is passed as {@link UNKNOWN_GENERATION} and the caller
|
|
1660
|
-
* decides whether that is acceptable; this method refuses it by default, because
|
|
1661
|
-
* "we do not know which attach this belongs to" is not a reason to write into
|
|
1662
|
-
* somebody else's trail.
|
|
1663
|
-
*
|
|
1664
|
-
* `attribution` is how the transport matched that frame to a request, and it is
|
|
1665
|
-
* the difference between `late` and `late-inferred` below: a frame that echoed
|
|
1666
|
-
* the id the client sent is confirmed, one with no id is filed against the
|
|
1667
|
-
* oldest tracked request by the daemon's in-order scheduling (R10-中2). Anything
|
|
1668
|
-
* this method writes is a late arrival by construction, so the window opens
|
|
1669
|
-
* whether or not the caller can say more — the default `unknown` is what the
|
|
1670
|
-
* session can see on its own, and it is *not* `in-order`: an unconfirmed
|
|
1671
|
-
* attribution must not read as a verified one.
|
|
1672
|
-
*/
|
|
1673
|
-
recordLateArrival(apply, admittedUnderGeneration, attribution = "unknown") {
|
|
1674
|
-
const turn = this.claimTrailTurn();
|
|
1675
|
-
return turn.acquire().then(() => {
|
|
1676
|
-
try {
|
|
1677
|
-
if (admittedUnderGeneration !== this.epoch) {
|
|
1678
|
-
return { written: false, generationAtWrite: this.epoch };
|
|
1679
|
-
}
|
|
1680
|
-
this.lateWindow = attribution;
|
|
1681
|
-
apply(this);
|
|
1682
|
-
return { written: true, generationAtWrite: this.epoch };
|
|
1683
|
-
} finally {
|
|
1684
|
-
this.lateWindow = null;
|
|
1685
|
-
turn.release();
|
|
1686
|
-
}
|
|
1687
|
-
});
|
|
1688
|
-
}
|
|
1689
|
-
/**
|
|
1690
|
-
* Claim the next position in this session's trail request order, so that
|
|
1691
|
-
* overlapping calls mutate and read `ids` in the order they were admitted
|
|
1692
|
-
* rather than in the order the daemon answered.
|
|
1693
|
-
*
|
|
1694
|
-
* Synchronous on purpose — a claim made after an `await` would itself be
|
|
1695
|
-
* ordered by completion. Callers hold a turn only across a `record()` /
|
|
1696
|
-
* `reset()` / `recentIds()`, never across an engine round trip, so the queue
|
|
1697
|
-
* can delay the moment a reply is built but never a request at the daemon.
|
|
1698
|
-
*/
|
|
1699
|
-
claimTrailTurn() {
|
|
1700
|
-
const ahead = [...this.openGates];
|
|
1701
|
-
const mine = openTrailGate();
|
|
1702
|
-
this.openGates.add(mine);
|
|
1703
|
-
let released = false;
|
|
1704
|
-
return {
|
|
1705
|
-
acquire: async () => {
|
|
1706
|
-
await Promise.all(ahead.map((gate) => gate.settled));
|
|
1707
|
-
},
|
|
1708
|
-
release: () => {
|
|
1709
|
-
if (released) {
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
|
-
released = true;
|
|
1713
|
-
this.openGates.delete(mine);
|
|
1714
|
-
mine.settle();
|
|
1715
|
-
}
|
|
1716
|
-
};
|
|
1717
|
-
}
|
|
1718
|
-
};
|
|
1719
|
-
function operationIds(value) {
|
|
1720
|
-
return collectOperationIds(value);
|
|
1721
|
-
}
|
|
1722
|
-
function collectOperationIds(value, depth = 0) {
|
|
1723
|
-
const out = [];
|
|
1724
|
-
if (depth > 3 || value === null || typeof value !== "object") {
|
|
1725
|
-
return out;
|
|
1726
|
-
}
|
|
1727
|
-
if (Array.isArray(value)) {
|
|
1728
|
-
for (const item of value) {
|
|
1729
|
-
out.push(...collectOperationIds(item, depth + 1));
|
|
1730
|
-
}
|
|
1731
|
-
return out;
|
|
1732
|
-
}
|
|
1733
|
-
const record = value;
|
|
1734
|
-
for (const key of Object.keys(record)) {
|
|
1735
|
-
const entry = record[key];
|
|
1736
|
-
if ((key === "operationId" || key === "evidenceId") && typeof entry === "string") {
|
|
1737
|
-
out.push(entry);
|
|
1738
|
-
} else {
|
|
1739
|
-
out.push(...collectOperationIds(entry, depth + 1));
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
return out;
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
1514
|
// src/evidence-report.ts
|
|
1746
1515
|
var PLACEHOLDER = "\u2014";
|
|
1747
1516
|
function reportTitle(pack) {
|
|
@@ -2494,8 +2263,8 @@ var ProjectRegistryError = class extends Error {
|
|
|
2494
2263
|
var PID_INT32_MAX = 2147483647;
|
|
2495
2264
|
var OptionalPidSchema = z5.number().int().min(1).max(PID_INT32_MAX).optional();
|
|
2496
2265
|
var OptionalDepthSchema = z5.number().int().min(1).max(10).optional();
|
|
2497
|
-
var OptionalRoleSchema = z5.string().max(
|
|
2498
|
-
var OptionalBundleIdSchema = z5.string().max(
|
|
2266
|
+
var OptionalRoleSchema = z5.string().max(SELECTOR_MAX_LENGTH).optional();
|
|
2267
|
+
var OptionalBundleIdSchema = z5.string().max(512).optional();
|
|
2499
2268
|
var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
2500
2269
|
var OptionalOperationIdSchema = z5.string().regex(OPERATION_ID_PATTERN2).optional();
|
|
2501
2270
|
var OptionalProjectIdSchema = z5.string().regex(/^prj_[0-9A-HJKMNP-TV-Z]{26}$/).optional();
|
|
@@ -2611,7 +2380,7 @@ var TOOL_SPECS = [
|
|
|
2611
2380
|
type: "object",
|
|
2612
2381
|
additionalProperties: false,
|
|
2613
2382
|
properties: {
|
|
2614
|
-
bundleId: { type: "string", maxLength:
|
|
2383
|
+
bundleId: { type: "string", maxLength: 512 },
|
|
2615
2384
|
pid: { type: "integer", minimum: 1, maximum: PID_INT32_MAX },
|
|
2616
2385
|
projectId: { type: "string", pattern: "^prj_[0-9A-HJKMNP-TV-Z]{26}$" }
|
|
2617
2386
|
},
|
|
@@ -2631,7 +2400,7 @@ var TOOL_SPECS = [
|
|
|
2631
2400
|
additionalProperties: false,
|
|
2632
2401
|
properties: {
|
|
2633
2402
|
maxDepth: { type: "integer", minimum: 1, maximum: 10 },
|
|
2634
|
-
role: { type: "string", maxLength:
|
|
2403
|
+
role: { type: "string", maxLength: SELECTOR_MAX_LENGTH }
|
|
2635
2404
|
}
|
|
2636
2405
|
},
|
|
2637
2406
|
validate: zodBridge(ObserveArgs)
|
|
@@ -3047,7 +2816,28 @@ async function readTrail(session, turn, limit) {
|
|
|
3047
2816
|
turn.release();
|
|
3048
2817
|
}
|
|
3049
2818
|
}
|
|
3050
|
-
|
|
2819
|
+
function shellFaultRemedy(engineMethod) {
|
|
2820
|
+
const readRoute = "read what happened instead of performing it again: gp_recent_reports lists the operations this session recorded, gp_last_evidence {operationId} returns one in full, and gp_probe_status reads the daemon's own self-report on this same connection \u2014 all three are requests this shell does not treat as unsafe to repeat";
|
|
2821
|
+
const reportRoute = "this fault was raised inside the MCP shell, so it is in no daemon log: this process's stderr belongs to whoever started this server, and that line is the only text naming the failure, so hand it to them verbatim. A daemon restart is not authorised by an answer the daemon never made";
|
|
2822
|
+
if (isReplayUnsafeMethod(engineMethod)) {
|
|
2823
|
+
return `this is a defect in this shell, not an answer from the engine, so it measures nothing about whether '${engineMethod}' reached the daemon or the user's screen. Do NOT re-issue ${engineMethod}: a re-issue can perform the same action a second time on the user's screen (and for attach, re-point the daemon and discard the trail an outstanding request is being recorded in), and this failure does not establish that the first copy did nothing. ${readRoute}. ${reportRoute}`;
|
|
2824
|
+
}
|
|
2825
|
+
if (!isNarrowableByRetryMethod(engineMethod)) {
|
|
2826
|
+
return `this is a defect in this shell, not an answer from the engine. '${engineMethod}' is a method whose re-issue writes daemon state \u2014 a fresh request appends to the bounded snapshot ring and can evict a baseline this caller already received \u2014 so treat it as a question to read around rather than one to send again: gp_snapshot lists stored ids, and ${readRoute}. ${reportRoute}`;
|
|
2827
|
+
}
|
|
2828
|
+
return `this is a defect in this shell, not an answer from the engine. '${engineMethod}' is not one of the methods this shell classifies as unsafe to repeat (the no-replay set is act/restore/attach, plus snapshot, whose re-issue writes daemon state), so sending this tool again is a real option rather than a side effect: do that once. If the fault repeats, ${readRoute}. ${reportRoute}`;
|
|
2829
|
+
}
|
|
2830
|
+
async function executeTool(spec, args, engine, session) {
|
|
2831
|
+
if (!session) {
|
|
2832
|
+
return {
|
|
2833
|
+
content: [{ type: "text", text: formatToolError(
|
|
2834
|
+
GP_E_INTERNAL,
|
|
2835
|
+
`executeTool was called for ${spec.name} without the session's trail`,
|
|
2836
|
+
"this shell recorded nothing for this call: executeTool requires the shared EvidenceAuditSession the server was built with (dispatch.ts's createTrackedMcpServer), because a trail handed to one call only is invisible to gp_recent_reports. Pass the server's session rather than omitting it"
|
|
2837
|
+
) }],
|
|
2838
|
+
isError: true
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
3051
2841
|
const checked = spec.validate(args);
|
|
3052
2842
|
if (!checked.ok) {
|
|
3053
2843
|
return {
|
|
@@ -3094,7 +2884,7 @@ async function runValidatedTool(spec, value, engine, session, turn) {
|
|
|
3094
2884
|
content.push({
|
|
3095
2885
|
type: "text",
|
|
3096
2886
|
text: reading.kind === "read" ? `${reading.summary}
|
|
3097
|
-
${caBundleDiagnosis(reading.ca)}
|
|
2887
|
+
${caBundleDiagnosis(reading.ca, updaterEnableCommand(reading.runtime))}
|
|
3098
2888
|
${runtimeSelfDiagnosis(reading.runtime, reading.stateFile)}` : reading.summary
|
|
3099
2889
|
});
|
|
3100
2890
|
}
|
|
@@ -3117,14 +2907,32 @@ ${runtimeSelfDiagnosis(reading.runtime, reading.stateFile)}` : reading.summary
|
|
|
3117
2907
|
content: [{ type: "text", text: formatToolError(
|
|
3118
2908
|
GP_E_INTERNAL,
|
|
3119
2909
|
`internal shell error: ${String(error)}`,
|
|
3120
|
-
|
|
2910
|
+
shellFaultRemedy(spec.engineMethod)
|
|
3121
2911
|
) }],
|
|
3122
2912
|
isError: true
|
|
3123
2913
|
};
|
|
3124
2914
|
}
|
|
3125
2915
|
}
|
|
3126
|
-
function
|
|
3127
|
-
|
|
2916
|
+
function shellPath(text) {
|
|
2917
|
+
return /[\s"'`\\]/.test(text) ? JSON.stringify(text) : text;
|
|
2918
|
+
}
|
|
2919
|
+
function absoluteNodeCommand(cliPath, suffix) {
|
|
2920
|
+
if (cliPath === null || !path4.isAbsolute(cliPath)) {
|
|
2921
|
+
return null;
|
|
2922
|
+
}
|
|
2923
|
+
return `node ${shellPath(cliPath)}${suffix}`;
|
|
2924
|
+
}
|
|
2925
|
+
function repoScript(name) {
|
|
2926
|
+
const here = path4.dirname(fileURLToPath2(import.meta.url));
|
|
2927
|
+
const candidate = path4.resolve(here, "..", "..", name);
|
|
2928
|
+
return fs4.existsSync(candidate) ? candidate : null;
|
|
2929
|
+
}
|
|
2930
|
+
function updaterEnableCommand(runtime) {
|
|
2931
|
+
const named2 = runtime.recorded ? runtime.cliPath : null;
|
|
2932
|
+
const command = absoluteNodeCommand(named2, " enable") ?? absoluteNodeCommand(repoScript(path4.join("updater", "cli.js")), " enable");
|
|
2933
|
+
return command === null ? "the `enable` step of this install's updater copy \u2014 its path is the `runtime.cliPath` the record names (printed on the line below, when the record carries one), and neither that record nor this build's own location gives an absolute path, so this remedy prints no command rather than one that resolves against a directory you are not standing in" : `\`${command}\``;
|
|
2934
|
+
}
|
|
2935
|
+
function caBundleDiagnosis(ca, enableCmd) {
|
|
3128
2936
|
if (!ca.recorded) {
|
|
3129
2937
|
return `ca trust bundle: this install has never recorded exporting the machine's trusted roots (the state file carries no caRoots), which is NOT the same as "the bundle is fine" \u2014 an install predating the export step looks exactly like this. node then verifies only against the CA list inside its own binary, so on a machine whose HTTPS is re-signed locally every auto-update run fails certificate verification and nothing on the surface says so. Remedy: re-run the GlassPane installer, or ${enableCmd}, either of which writes the roots to ${ca.bundlePath} and points the launchd agent and the panel at it. For a check run by hand in a terminal: export NODE_EXTRA_CA_CERTS=${ca.bundlePath} \u2014 but only once that file exists, and set before node starts, because a running process never picks the variable up.`;
|
|
3130
2938
|
}
|
|
@@ -3161,8 +2969,10 @@ function caBundleDiagnosis(ca) {
|
|
|
3161
2969
|
return provenance.length === 0 ? diagnosis : `${diagnosis} The record itself says: ${provenance.join("; ")}.`;
|
|
3162
2970
|
}
|
|
3163
2971
|
function runtimeSelfDiagnosis(runtime, stateFile) {
|
|
3164
|
-
const
|
|
3165
|
-
const
|
|
2972
|
+
const installerCommand = absoluteNodeCommand(repoScript("installer/cli.js"), "");
|
|
2973
|
+
const installScript = repoScript("install.sh");
|
|
2974
|
+
const installerCmd = installerCommand === null ? "the installer of the GlassPane checkout this machine was installed from \u2014 this build is not shipped beside it, so no absolute path can be named from here, and every command in that tree has to be run from inside it" : installScript === null ? `\`${installerCommand}\`` : `\`${installerCommand}\` (or \`${shellPath(installScript)}\`)`;
|
|
2975
|
+
const enableCmd = (cliPath) => absoluteNodeCommand(cliPath, ` enable --state-dir ${shellPath(stateRootOf(stateFile))} --json`);
|
|
3166
2976
|
const copy = (version, cliPath) => {
|
|
3167
2977
|
if (cliPath === null) {
|
|
3168
2978
|
return version === null ? "the record names neither a version nor a path for it" : `version ${version}, and the record names no path for it`;
|
|
@@ -3260,7 +3070,17 @@ async function captureView(args, context) {
|
|
|
3260
3070
|
isError: true
|
|
3261
3071
|
};
|
|
3262
3072
|
}
|
|
3263
|
-
|
|
3073
|
+
if (typeof body.mimeType !== "string" || body.mimeType.length === 0) {
|
|
3074
|
+
return {
|
|
3075
|
+
content: [{ type: "text", text: formatToolError(
|
|
3076
|
+
GP_E_INTERNAL,
|
|
3077
|
+
`capture_view answered without a mimeType (got ${JSON.stringify(body.mimeType ?? null)})`,
|
|
3078
|
+
"do not read this image as any particular format: the engine did not name one, and this shell will not fill the field in. This is a read-only capture, so sending it again performs nothing \u2014 if it repeats, report the frame shape, which is a protocol disagreement between two builds rather than a dead daemon"
|
|
3079
|
+
) }],
|
|
3080
|
+
isError: true
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
const mimeType = body.mimeType;
|
|
3264
3084
|
const summary = canonicalJson({
|
|
3265
3085
|
mimeType,
|
|
3266
3086
|
byteCount: body.byteCount,
|
|
@@ -3292,7 +3112,7 @@ async function captureView(args, context) {
|
|
|
3292
3112
|
content: [{ type: "text", text: formatToolError(
|
|
3293
3113
|
GP_E_INTERNAL,
|
|
3294
3114
|
`internal shell error: ${String(error)}`,
|
|
3295
|
-
"
|
|
3115
|
+
shellFaultRemedy("capture_view")
|
|
3296
3116
|
) }],
|
|
3297
3117
|
isError: true
|
|
3298
3118
|
};
|
|
@@ -3393,7 +3213,7 @@ function projectErrorRemedy(code) {
|
|
|
3393
3213
|
return "this process has no entry in the password database, so neither it nor the daemon can know which user's state directory is meant; run the shell as a user that has one (`id -u` / `dscl . -read /Users/<name> NFSHomeDirectory`). Nothing in ~/.glasspane needs repairing for this";
|
|
3394
3214
|
}
|
|
3395
3215
|
if (code === GP_E_INTERNAL) {
|
|
3396
|
-
return `the projects file is the problem, not your arguments: the message above names its path, so read it with
|
|
3216
|
+
return `the projects file is the problem, not your arguments: the message above names its path, so read it with \`/usr/bin/plutil -p <that path>\` or \`/bin/cat <that path>\` \u2014 both absolute, so they run from whatever directory the reader is in and need nothing that may be off PATH \u2014 repair or restore the damaged entry, and retry \u2014 or set ${FORCE_OVERWRITE_ENV}=1 and call gp_project_set to rebuild the registry from scratch (the unloadable file is moved aside as <path>.unreadable-<id>, never deleted, and every entry still in it is lost). Do not retry the same read-only call unchanged: it will keep failing.`;
|
|
3397
3217
|
}
|
|
3398
3218
|
return "check the tool's input schema and retry";
|
|
3399
3219
|
}
|
|
@@ -3468,7 +3288,7 @@ function renderTrailIds(ids, format) {
|
|
|
3468
3288
|
}
|
|
3469
3289
|
function describeFetchFailure(error) {
|
|
3470
3290
|
if (error instanceof EngineCallError) {
|
|
3471
|
-
return
|
|
3291
|
+
return formatToolErrorShape(error.toBody());
|
|
3472
3292
|
}
|
|
3473
3293
|
const mapped = mapEvidenceReadError(error);
|
|
3474
3294
|
if (mapped !== void 0) {
|
|
@@ -3589,8 +3409,8 @@ async function recentReports(args, context) {
|
|
|
3589
3409
|
return {
|
|
3590
3410
|
content: [{ type: "text", text: formatToolError(
|
|
3591
3411
|
GP_E_NO_EVIDENCE,
|
|
3592
|
-
|
|
3593
|
-
"the
|
|
3412
|
+
`no pack is reachable in the daemon history for any of the ${ids.length} ${ids.length === 1 ? "operation" : "operations"} this shell asked about: each was answered GP_E_NO_EVIDENCE (${renderTrailIds(skipped, format)})`,
|
|
3413
|
+
"these operations are recorded in this session's trail, and the daemon holding no pack for them is a fact about its bounded history, not evidence that they did not run \u2014 this call measured nothing about a restart, so no cause is claimed here. Read one of them by id with gp_last_evidence {operationId} / gp_export_evidence {operationId}, or ask again with a smaller limit (gp_recent_reports advertises 1\u202620). Do not re-run gp_act to regenerate what the trail already holds"
|
|
3594
3414
|
) }],
|
|
3595
3415
|
isError: true
|
|
3596
3416
|
};
|
|
@@ -3613,7 +3433,7 @@ async function recentReports(args, context) {
|
|
|
3613
3433
|
sections2.push(body);
|
|
3614
3434
|
return { content: [{ type: "text", text: sections2.join("\n") }], isError: false };
|
|
3615
3435
|
}
|
|
3616
|
-
function mapAuditError(error) {
|
|
3436
|
+
function mapAuditError(error, engineMethod = "last_evidence") {
|
|
3617
3437
|
if (error instanceof EngineCallError) {
|
|
3618
3438
|
return {
|
|
3619
3439
|
content: [{ type: "text", text: formatToolErrorShape(error.toBody()) }],
|
|
@@ -3628,7 +3448,7 @@ function mapAuditError(error) {
|
|
|
3628
3448
|
content: [{ type: "text", text: formatToolError(
|
|
3629
3449
|
GP_E_INTERNAL,
|
|
3630
3450
|
`internal shell error: ${String(error)}`,
|
|
3631
|
-
|
|
3451
|
+
shellFaultRemedy(engineMethod)
|
|
3632
3452
|
) }],
|
|
3633
3453
|
isError: true
|
|
3634
3454
|
};
|
|
@@ -3657,6 +3477,243 @@ function engineWithPayloadAdvice(engine, spec, toolArgs = {}) {
|
|
|
3657
3477
|
return wrapped;
|
|
3658
3478
|
}
|
|
3659
3479
|
|
|
3480
|
+
// src/audit-session.ts
|
|
3481
|
+
var AUDIT_HISTORY_LIMIT = 20;
|
|
3482
|
+
var UNKNOWN_GENERATION = -1;
|
|
3483
|
+
function openTrailGate() {
|
|
3484
|
+
let settle;
|
|
3485
|
+
const settled = new Promise((resolve) => {
|
|
3486
|
+
settle = () => resolve(void 0);
|
|
3487
|
+
});
|
|
3488
|
+
return { settled, settle };
|
|
3489
|
+
}
|
|
3490
|
+
var EvidenceAuditSession = class {
|
|
3491
|
+
ids = [];
|
|
3492
|
+
epoch = 0;
|
|
3493
|
+
attachedAppKey = null;
|
|
3494
|
+
/**
|
|
3495
|
+
* Turns claimed but not released. This, not a "latest gate", is what a new
|
|
3496
|
+
* claim waits for: a turn that abandons the trail (its engine call failed
|
|
3497
|
+
* before any mutation) releases *out of order*, and a chain that only looked
|
|
3498
|
+
* at the immediately preceding turn would let everything behind it run while
|
|
3499
|
+
* an older, still-outstanding act had not written yet — the exact completion
|
|
3500
|
+
* order the turn machinery exists to remove.
|
|
3501
|
+
*/
|
|
3502
|
+
openGates = /* @__PURE__ */ new Set();
|
|
3503
|
+
/**
|
|
3504
|
+
* How each id in {@link ids} got here. Keyed by the id, because that is what a
|
|
3505
|
+
* reader holds; the map is trimmed with the list and cleared when the trail
|
|
3506
|
+
* restarts, so an id can never outlive the provenance that describes it.
|
|
3507
|
+
*/
|
|
3508
|
+
provenance = /* @__PURE__ */ new Map();
|
|
3509
|
+
/**
|
|
3510
|
+
* Non-null while {@link recordLateArrival} holds the trail, and says how the
|
|
3511
|
+
* frame being written was attributed. This is what lets a plain
|
|
3512
|
+
* `trail.record(result)` inside that callback be filed as a late arrival without
|
|
3513
|
+
* the caller having to remember to say so: the write happens inside a window only
|
|
3514
|
+
* that method opens, so the mechanism is visible here rather than being a claim
|
|
3515
|
+
* somebody must repeat at every call site.
|
|
3516
|
+
*/
|
|
3517
|
+
lateWindow = null;
|
|
3518
|
+
/**
|
|
3519
|
+
* Record every operationId/evidenceId found in an engine result frame, tagged
|
|
3520
|
+
* with the provenance of the path that is recording it (see
|
|
3521
|
+
* {@link TrailProvenance}): `in-order` from a call that held its own trail turn,
|
|
3522
|
+
* `late`/`late-inferred` from inside {@link recordLateArrival}.
|
|
3523
|
+
*/
|
|
3524
|
+
record(result) {
|
|
3525
|
+
const source = this.lateWindow === null ? "in-order" : this.lateWindow === "arrival-order" ? "late-inferred" : "late";
|
|
3526
|
+
for (const id of collectOperationIds(result)) {
|
|
3527
|
+
if (!this.ids.includes(id)) {
|
|
3528
|
+
this.ids.push(id);
|
|
3529
|
+
this.provenance.set(id, source);
|
|
3530
|
+
if (this.ids.length > AUDIT_HISTORY_LIMIT) {
|
|
3531
|
+
const evicted = this.ids.splice(0, this.ids.length - AUDIT_HISTORY_LIMIT);
|
|
3532
|
+
for (const gone of evicted) {
|
|
3533
|
+
this.provenance.delete(gone);
|
|
3534
|
+
}
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
/**
|
|
3540
|
+
* How one id in this trail got recorded, or null when the trail does not hold
|
|
3541
|
+
* it — never recorded, evicted past {@link AUDIT_HISTORY_LIMIT}, or cleared by a
|
|
3542
|
+
* {@link restart}. A reader that lists ids for an agent renders
|
|
3543
|
+
* {@link TRAIL_PROVENANCE_NOTES} for anything that is not `in-order`: an id
|
|
3544
|
+
* pulled from a frame this shell only *guessed* an owner for must not appear
|
|
3545
|
+
* beside one it matched by id (R10-中2).
|
|
3546
|
+
*
|
|
3547
|
+
* Read off the map alone, so the map *is* the record of what the trail holds: an
|
|
3548
|
+
* id whose provenance outlives its own entry would come back labelled as if it
|
|
3549
|
+
* were still listed, and the eviction and the restart are the two places that
|
|
3550
|
+
* would have to remember to delete. `test/engine-client.test.mjs` pins both.
|
|
3551
|
+
*/
|
|
3552
|
+
provenanceOf(id) {
|
|
3553
|
+
return this.provenance.get(id) ?? null;
|
|
3554
|
+
}
|
|
3555
|
+
/** The ids currently filed as inferred; see {@link TrailProvenance}. */
|
|
3556
|
+
inferredIds() {
|
|
3557
|
+
return this.ids.filter((id) => this.provenance.get(id) === "late-inferred");
|
|
3558
|
+
}
|
|
3559
|
+
/**
|
|
3560
|
+
* The trail's generation number. Every {@link restart} bumps it.
|
|
3561
|
+
*
|
|
3562
|
+
* It exists so a reply that arrives *after* its caller was already answered
|
|
3563
|
+
* can be judged against the trail it was written into: {@link recordLateArrival}
|
|
3564
|
+
* takes the epoch the request was admitted under, and a mismatch means a
|
|
3565
|
+
* re-attach happened in between. Without it, a late write lands in a *new*
|
|
3566
|
+
* session's trail — an operationId from the previous app that
|
|
3567
|
+
* `gp_recent_reports` then fetches and renders as if it belonged to this one
|
|
3568
|
+
* (R8b-高1, measured: `['op_OLDAPP']` in the trail after attaching a second
|
|
3569
|
+
* app).
|
|
3570
|
+
*/
|
|
3571
|
+
get generation() {
|
|
3572
|
+
return this.epoch;
|
|
3573
|
+
}
|
|
3574
|
+
/** The app the trail currently belongs to, or null before the first attach. */
|
|
3575
|
+
get attachedApp() {
|
|
3576
|
+
return this.attachedAppKey;
|
|
3577
|
+
}
|
|
3578
|
+
/**
|
|
3579
|
+
* A successful attach: the trail restarts **only when the attached app
|
|
3580
|
+
* changed**, because that is the daemon's own rule —
|
|
3581
|
+
* `EngineCore.attach` clears `history` under
|
|
3582
|
+
* `if attachedApp != app`, and its comment says re-attach to the same app is
|
|
3583
|
+
* idempotent. Clearing on *every* attach (what this used to do) is strictly
|
|
3584
|
+
* worse in two directions: an idempotent re-attach wiped operations that were
|
|
3585
|
+
* really in the trail (including one recorded by a late reply, which put
|
|
3586
|
+
* `gp_recent_reports` back to "run gp_act first"), and — because the clear
|
|
3587
|
+
* happens whenever the reply lands — it could also drop the record of an act
|
|
3588
|
+
* the daemon still has.
|
|
3589
|
+
*
|
|
3590
|
+
* `appKey` must be built from every stored property of Swift's `AttachedApp`
|
|
3591
|
+
* (`pid`, `bundleId`, `appName`); a key that ignored `pid` would keep the
|
|
3592
|
+
* trail across an app restart whose history the daemon did clear. The gate
|
|
3593
|
+
* that pins this to the Swift struct is
|
|
3594
|
+
* `test/attach-identity.test.mjs`.
|
|
3595
|
+
*
|
|
3596
|
+
* Returns whether the trail was restarted. A null `appKey` means the attach
|
|
3597
|
+
* result did not identify the app at all, which is treated as a change: an
|
|
3598
|
+
* unknown identity cannot be *shown* to be the same app, and a stale trail is
|
|
3599
|
+
* the failure that misattributes evidence.
|
|
3600
|
+
*/
|
|
3601
|
+
restart(appKey) {
|
|
3602
|
+
if (appKey !== null && appKey === this.attachedAppKey) {
|
|
3603
|
+
return false;
|
|
3604
|
+
}
|
|
3605
|
+
this.attachedAppKey = appKey;
|
|
3606
|
+
this.ids = [];
|
|
3607
|
+
this.provenance.clear();
|
|
3608
|
+
this.epoch += 1;
|
|
3609
|
+
return true;
|
|
3610
|
+
}
|
|
3611
|
+
/** The most recent `limit` recorded operationIds, oldest first. */
|
|
3612
|
+
recentIds(limit) {
|
|
3613
|
+
return this.ids.slice(-limit);
|
|
3614
|
+
}
|
|
3615
|
+
/**
|
|
3616
|
+
* Record a frame that arrives with **no request of the caller still holding a
|
|
3617
|
+
* turn**: a late engine reply (R8-高3, `EngineJsonRpcClient.onLateReply`). Its
|
|
3618
|
+
* own turn was released the moment the timeout was reported to the agent, so
|
|
3619
|
+
* there is no request-order position left to write at.
|
|
3620
|
+
*
|
|
3621
|
+
* It still goes through the turn machinery rather than a bare `record()`,
|
|
3622
|
+
* because a direct write could land *between* two admitted calls and reorder
|
|
3623
|
+
* the trail against the requests the agent made — the exact thing the turns
|
|
3624
|
+
* exist to prevent. Claiming on arrival gives the only honest position left:
|
|
3625
|
+
* behind whatever is still outstanding, and ordered among late arrivals by
|
|
3626
|
+
* when they arrived.
|
|
3627
|
+
*
|
|
3628
|
+
* `admittedUnderGeneration` is compared **inside** that wait (see the body):
|
|
3629
|
+
* the position and the epoch can only be judged at the moment the write
|
|
3630
|
+
* actually happens. A reply whose request carried no generation cannot be
|
|
3631
|
+
* placed either — so it is passed as {@link UNKNOWN_GENERATION} and the caller
|
|
3632
|
+
* decides whether that is acceptable; this method refuses it by default, because
|
|
3633
|
+
* "we do not know which attach this belongs to" is not a reason to write into
|
|
3634
|
+
* somebody else's trail.
|
|
3635
|
+
*
|
|
3636
|
+
* `attribution` is how the transport matched that frame to a request, and it is
|
|
3637
|
+
* the difference between `late` and `late-inferred` below: a frame that echoed
|
|
3638
|
+
* the id the client sent is confirmed, one with no id is filed against the
|
|
3639
|
+
* oldest tracked request by the daemon's in-order scheduling (R10-中2). Anything
|
|
3640
|
+
* this method writes is a late arrival by construction, so the window opens
|
|
3641
|
+
* whether or not the caller can say more — the default `unknown` is what the
|
|
3642
|
+
* session can see on its own, and it is *not* `in-order`: an unconfirmed
|
|
3643
|
+
* attribution must not read as a verified one.
|
|
3644
|
+
*/
|
|
3645
|
+
recordLateArrival(apply, admittedUnderGeneration, attribution = "unknown") {
|
|
3646
|
+
const turn = this.claimTrailTurn();
|
|
3647
|
+
return turn.acquire().then(() => {
|
|
3648
|
+
try {
|
|
3649
|
+
if (admittedUnderGeneration !== this.epoch) {
|
|
3650
|
+
return { written: false, generationAtWrite: this.epoch };
|
|
3651
|
+
}
|
|
3652
|
+
this.lateWindow = attribution;
|
|
3653
|
+
apply(this);
|
|
3654
|
+
return { written: true, generationAtWrite: this.epoch };
|
|
3655
|
+
} finally {
|
|
3656
|
+
this.lateWindow = null;
|
|
3657
|
+
turn.release();
|
|
3658
|
+
}
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3661
|
+
/**
|
|
3662
|
+
* Claim the next position in this session's trail request order, so that
|
|
3663
|
+
* overlapping calls mutate and read `ids` in the order they were admitted
|
|
3664
|
+
* rather than in the order the daemon answered.
|
|
3665
|
+
*
|
|
3666
|
+
* Synchronous on purpose — a claim made after an `await` would itself be
|
|
3667
|
+
* ordered by completion. Callers hold a turn only across a `record()` /
|
|
3668
|
+
* `reset()` / `recentIds()`, never across an engine round trip, so the queue
|
|
3669
|
+
* can delay the moment a reply is built but never a request at the daemon.
|
|
3670
|
+
*/
|
|
3671
|
+
claimTrailTurn() {
|
|
3672
|
+
const ahead = [...this.openGates];
|
|
3673
|
+
const mine = openTrailGate();
|
|
3674
|
+
this.openGates.add(mine);
|
|
3675
|
+
let released = false;
|
|
3676
|
+
return {
|
|
3677
|
+
acquire: async () => {
|
|
3678
|
+
await Promise.all(ahead.map((gate) => gate.settled));
|
|
3679
|
+
},
|
|
3680
|
+
release: () => {
|
|
3681
|
+
if (released) {
|
|
3682
|
+
return;
|
|
3683
|
+
}
|
|
3684
|
+
released = true;
|
|
3685
|
+
this.openGates.delete(mine);
|
|
3686
|
+
mine.settle();
|
|
3687
|
+
}
|
|
3688
|
+
};
|
|
3689
|
+
}
|
|
3690
|
+
};
|
|
3691
|
+
function operationIds(value) {
|
|
3692
|
+
return collectOperationIds(value);
|
|
3693
|
+
}
|
|
3694
|
+
function collectOperationIds(value, depth = 0) {
|
|
3695
|
+
const out = [];
|
|
3696
|
+
if (depth > 3 || value === null || typeof value !== "object") {
|
|
3697
|
+
return out;
|
|
3698
|
+
}
|
|
3699
|
+
if (Array.isArray(value)) {
|
|
3700
|
+
for (const item of value) {
|
|
3701
|
+
out.push(...collectOperationIds(item, depth + 1));
|
|
3702
|
+
}
|
|
3703
|
+
return out;
|
|
3704
|
+
}
|
|
3705
|
+
const record = value;
|
|
3706
|
+
for (const key of Object.keys(record)) {
|
|
3707
|
+
const entry = record[key];
|
|
3708
|
+
if ((key === "operationId" || key === "evidenceId") && typeof entry === "string") {
|
|
3709
|
+
out.push(entry);
|
|
3710
|
+
} else {
|
|
3711
|
+
out.push(...collectOperationIds(entry, depth + 1));
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
return out;
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3660
3717
|
// src/dispatch.ts
|
|
3661
3718
|
var JSONRPC = "2.0";
|
|
3662
3719
|
var MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
@@ -3665,7 +3722,7 @@ var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
|
3665
3722
|
"2025-03-26",
|
|
3666
3723
|
MCP_PROTOCOL_VERSION
|
|
3667
3724
|
];
|
|
3668
|
-
var SERVER_INFO = { name: "glasspane-mcp", version: "1.
|
|
3725
|
+
var SERVER_INFO = { name: "glasspane-mcp", version: "1.12.0" };
|
|
3669
3726
|
var PARSE_ERROR = -32700;
|
|
3670
3727
|
var INVALID_REQUEST = -32600;
|
|
3671
3728
|
var METHOD_NOT_FOUND = -32601;
|
|
@@ -3900,6 +3957,32 @@ A daemon started by hand with --state-dir has no launchd job to read: pass it
|
|
|
3900
3957
|
<state-dir>/daemon.sock.
|
|
3901
3958
|
`;
|
|
3902
3959
|
var DRAIN_WAIT_MS = 3e4;
|
|
3960
|
+
var WRITE_STALL_MS = 10;
|
|
3961
|
+
var WRITE_STALL_LIMIT = 1e3;
|
|
3962
|
+
function writeDescriptor(fd, text) {
|
|
3963
|
+
const buffer = Buffer.from(text, "utf8");
|
|
3964
|
+
let offset = 0;
|
|
3965
|
+
let stalls = 0;
|
|
3966
|
+
while (offset < buffer.length) {
|
|
3967
|
+
let written;
|
|
3968
|
+
try {
|
|
3969
|
+
written = fs5.writeSync(fd, buffer, offset, buffer.length - offset);
|
|
3970
|
+
} catch (error) {
|
|
3971
|
+
const code = error.code;
|
|
3972
|
+
if (code === "EAGAIN" && stalls < WRITE_STALL_LIMIT) {
|
|
3973
|
+
stalls++;
|
|
3974
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, WRITE_STALL_MS);
|
|
3975
|
+
continue;
|
|
3976
|
+
}
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
if (written <= 0) {
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
offset += written;
|
|
3983
|
+
stalls = 0;
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3903
3986
|
function logNote(text) {
|
|
3904
3987
|
try {
|
|
3905
3988
|
process2.stderr.write(`[glasspane-mcp] ${text}
|
|
@@ -3922,11 +4005,11 @@ function main() {
|
|
|
3922
4005
|
try {
|
|
3923
4006
|
options = parseArgs(process2.argv.slice(2));
|
|
3924
4007
|
} catch (error) {
|
|
3925
|
-
|
|
4008
|
+
writeDescriptor(2, String(error) + "\n\n" + USAGE);
|
|
3926
4009
|
process2.exit(2);
|
|
3927
4010
|
}
|
|
3928
4011
|
if (options.help) {
|
|
3929
|
-
|
|
4012
|
+
writeDescriptor(1, USAGE);
|
|
3930
4013
|
process2.exit(0);
|
|
3931
4014
|
}
|
|
3932
4015
|
process2.stdin.setEncoding("utf8");
|
|
@@ -3936,7 +4019,8 @@ function main() {
|
|
|
3936
4019
|
identity: { version: SERVER_INFO.version }
|
|
3937
4020
|
});
|
|
3938
4021
|
} catch (error) {
|
|
3939
|
-
|
|
4022
|
+
writeDescriptor(
|
|
4023
|
+
2,
|
|
3940
4024
|
`failed to create engine client on ${options.socketPath}: ${String(error)}
|
|
3941
4025
|
`
|
|
3942
4026
|
);
|
|
@@ -3977,7 +4061,7 @@ function main() {
|
|
|
3977
4061
|
process2.stdin.on("data", (chunk) => reader.push(chunk));
|
|
3978
4062
|
process2.stdin.on("error", (error) => {
|
|
3979
4063
|
logNote(`stdin failed: ${error.message}`);
|
|
3980
|
-
shutdown();
|
|
4064
|
+
queue.push(async () => shutdown());
|
|
3981
4065
|
});
|
|
3982
4066
|
process2.stdin.on("close", () => queue.push(async () => shutdown()));
|
|
3983
4067
|
process2.on("SIGINT", shutdown);
|
package/dist/tools.d.ts
CHANGED
|
@@ -373,6 +373,24 @@ export declare function isNarrowableByRetryMethod(method: string): boolean;
|
|
|
373
373
|
* is the transport's, which orders no re-send.
|
|
374
374
|
*/
|
|
375
375
|
export declare function toolAuthorsTimeoutAdvice(spec: ToolSpec, method: string, params: Record<string, unknown>): boolean;
|
|
376
|
+
/**
|
|
377
|
+
* The remedy for a fault raised by *this shell*, on a call to `engineMethod`.
|
|
378
|
+
*
|
|
379
|
+
* This replaced "see the MCP server logs and retry" at all three internal-fault
|
|
380
|
+
* sites, and both halves of that sentence were wrong (review 2026-10-09 finding
|
|
381
|
+
* 8). "See the MCP server logs" is not something the caller can do: this process's
|
|
382
|
+
* stderr is held by whoever started the server, and a fault raised here is in no
|
|
383
|
+
* daemon log either, so the instruction sent the agent to look at a file that
|
|
384
|
+
* cannot contain the answer. And "retry" is an order to put a second copy of the
|
|
385
|
+
* request on the wire for methods whose re-issue this shell already refuses to
|
|
386
|
+
* endorse — `act`/`restore`/`attach` may still be performing their change on the
|
|
387
|
+
* user's screen, and a shell-side fault says nothing about whether the first copy
|
|
388
|
+
* went out. The classification is read from the transport's own lists
|
|
389
|
+
* ({@link isReplayUnsafeMethod}, {@link isNarrowableByRetryMethod}) rather than a
|
|
390
|
+
* third copy of the names, which is what keeps this answer from drifting back into
|
|
391
|
+
* "just retry the click".
|
|
392
|
+
*/
|
|
393
|
+
export declare function shellFaultRemedy(engineMethod: string): string;
|
|
376
394
|
/**
|
|
377
395
|
* Execute a tool against the engine client. Validates arguments against
|
|
378
396
|
* the tool's zod schema (isError + GP_E_BAD_PARAMS on failure), then either
|
|
@@ -383,7 +401,20 @@ export declare function toolAuthorsTimeoutAdvice(spec: ToolSpec, method: string,
|
|
|
383
401
|
* before it does (see the trail-ordering block above); the engine call that
|
|
384
402
|
* precedes that wait stays concurrent with every other outstanding request.
|
|
385
403
|
*/
|
|
386
|
-
export declare function executeTool(spec: ToolSpec, args: unknown, engine: EngineJsonRpcClient,
|
|
404
|
+
export declare function executeTool(spec: ToolSpec, args: unknown, engine: EngineJsonRpcClient,
|
|
405
|
+
/**
|
|
406
|
+
* The session whose trail this call records into and reads back. **Required**,
|
|
407
|
+
* and it used to be `= new EvidenceAuditSession()`: a caller that omitted the
|
|
408
|
+
* argument then recorded its `operationId` into a trail nobody else held a
|
|
409
|
+
* reference to, so the next `gp_recent_reports` answered
|
|
410
|
+
* `GP_E_NO_EVIDENCE … no operations recorded in this session` for an `act` that
|
|
411
|
+
* had really run — the reading that makes an agent press it a second time on the
|
|
412
|
+
* user's screen. That is the same defect `dispatch.ts`'s `McpServerDeps.session`
|
|
413
|
+
* documents as removed, and it is checked at runtime for the same reason: every
|
|
414
|
+
* meaningful caller here is JavaScript (`dist` consumers, the smoke scripts, the
|
|
415
|
+
* tests), where an omitted argument compiles forever and fails silently.
|
|
416
|
+
*/
|
|
417
|
+
session: EvidenceAuditSession): Promise<ToolResult>;
|
|
387
418
|
/**
|
|
388
419
|
* Strongly validates the engine's last_evidence result (§6.3) and returns the
|
|
389
420
|
* parsed pack together with the schema label the archive *carried*. The engine
|
package/package.json
CHANGED