glasspane-mcp 1.12.2 → 1.13.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 +29 -2
- package/dist/errors.d.ts +6 -0
- package/dist/http-gateway-cli.js +40 -4
- package/dist/http-gateway.js +40 -4
- package/dist/index.js +81 -7
- package/dist/io.d.ts +15 -1
- package/dist/tools.d.ts +8 -0
- 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.13.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
|
@@ -140,7 +140,16 @@ 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
142
|
/**
|
|
143
|
-
*
|
|
143
|
+
* `hello.capabilities` 的第一个读者(2026-10-11)。
|
|
144
|
+
*
|
|
145
|
+
* 这份清单由 daemon 从 `EngineMethod` 派生(`FrameCodec.capabilityWireNames`),可此前
|
|
146
|
+
* **没有任何一方读它**:面板的解析器只取 version/protocolVersion/pid/identity/permissions,
|
|
147
|
+
* 这一层只比 protocolVersion。没有读者就等于没有镜像——三处真源(kernel schema、daemon 方法表、
|
|
148
|
+
* 本壳的 TOOL_SPECS)漂了也不会有人说一句,而"清单存在但没人核"正是这份代码在别处反复拒绝的
|
|
149
|
+
* 那种自证。返回 null 表示无话可说(两边一致),否则给出要说的那句话。
|
|
150
|
+
*/
|
|
151
|
+
export declare function describeCapabilityDrift(advertised: readonly string[] | null, forwarded: readonly string[]): string | null;
|
|
152
|
+
/**
|
|
144
153
|
* replaying them is not unsafe in the sense {@link REPLAY_UNSAFE_REASONS} means
|
|
145
154
|
* (no operation of the user's is performed a second time). They are the other half
|
|
146
155
|
* of the answer to "may this be narrowed by sending it again?", and they are kept
|
|
@@ -309,6 +318,14 @@ export declare class EngineJsonRpcClient {
|
|
|
309
318
|
private readonly callerCeilingMs;
|
|
310
319
|
/** Whom a timeout's remedy is being written for; see {@link ShellSurface}. */
|
|
311
320
|
private readonly surface;
|
|
321
|
+
/**
|
|
322
|
+
* The daemon methods this shell forwards, injected by the composition root
|
|
323
|
+
* from `TOOL_SPECS` (never hand-copied — a copied list is the drift this
|
|
324
|
+
* exists to catch). Empty means "no expectation was wired", and the
|
|
325
|
+
* reconciliation stays silent rather than reporting every capability as
|
|
326
|
+
* unexposed; `index.ts` supplies the real list.
|
|
327
|
+
*/
|
|
328
|
+
private readonly expectedDaemonMethods;
|
|
312
329
|
/**
|
|
313
330
|
* Every mutable piece of this client's state, in one object that is *never*
|
|
314
331
|
* reassigned through `this`.
|
|
@@ -339,7 +356,15 @@ export declare class EngineJsonRpcClient {
|
|
|
339
356
|
*/
|
|
340
357
|
callerCeilingMs?: number,
|
|
341
358
|
/** Whom a timeout's remedy is being written for; see {@link ShellSurface}. */
|
|
342
|
-
surface?: ShellSurface
|
|
359
|
+
surface?: ShellSurface,
|
|
360
|
+
/**
|
|
361
|
+
* The daemon methods this shell forwards, injected by the composition root
|
|
362
|
+
* from `TOOL_SPECS` (never hand-copied — a copied list is the drift this
|
|
363
|
+
* exists to catch). Empty means "no expectation was wired", and the
|
|
364
|
+
* reconciliation stays silent rather than reporting every capability as
|
|
365
|
+
* unexposed; `index.ts` supplies the real list.
|
|
366
|
+
*/
|
|
367
|
+
expectedDaemonMethods?: readonly string[]);
|
|
343
368
|
/**
|
|
344
369
|
* Sink for facts that cannot be attached to any request of the caller: a
|
|
345
370
|
* late reply, a frame with an id this client never issued, a daemon that
|
|
@@ -462,6 +487,8 @@ export declare function unixSocketEngineClient(socketPath: string, options?: {
|
|
|
462
487
|
callerCeilingMs?: number;
|
|
463
488
|
/** Pass `"http"` from the gateway: its callers have no operation trail. */
|
|
464
489
|
surface?: ShellSurface;
|
|
490
|
+
/** Daemon methods this shell forwards; see {@link EngineJsonRpcClient}'s last parameter. */
|
|
491
|
+
expectedDaemonMethods?: readonly string[];
|
|
465
492
|
}): EngineJsonRpcClient;
|
|
466
493
|
/**
|
|
467
494
|
* Build a client over an existing LineIo (used by tests with a fake
|
package/dist/errors.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export declare const GP_E_NO_EVIDENCE = "GP_E_NO_EVIDENCE";
|
|
|
9
9
|
export declare const GP_E_INTERNAL = "GP_E_INTERNAL";
|
|
10
10
|
export declare const GP_E_PROJECT_LIMIT = "GP_E_PROJECT_LIMIT";
|
|
11
11
|
export declare const GP_E_NOT_FOUND = "GP_E_NOT_FOUND";
|
|
12
|
+
/** Shell-only: the table changed between our read and our write, so we refused
|
|
13
|
+
* to overwrite. The daemon has no such code (it owns one writer), and every other
|
|
14
|
+
* code this shell raises for itself is listed with it in
|
|
15
|
+
* `test/remedy-surface.test.mjs`'s SHELL_ONLY set — a code outside the vocabulary
|
|
16
|
+
* with no remedy arm reads to an agent as "retry the same write". */
|
|
17
|
+
export declare const GP_E_REGISTRY_DRIFT = "GP_E_REGISTRY_DRIFT";
|
|
12
18
|
/**
|
|
13
19
|
* Codes this shell raises for itself, in one place so a string literal cannot
|
|
14
20
|
* exist twice with different spellings (X-15).
|
package/dist/http-gateway-cli.js
CHANGED
|
@@ -141,6 +141,7 @@ var StreamLineIo = class {
|
|
|
141
141
|
this.input.destroy();
|
|
142
142
|
}
|
|
143
143
|
};
|
|
144
|
+
var STDIO_WRITE_BACKLOG_BYTES = 32 * 1024 * 1024;
|
|
144
145
|
|
|
145
146
|
// src/errors.ts
|
|
146
147
|
var GP_E_ENGINE_UNREACHABLE = "GP_E_ENGINE_UNREACHABLE";
|
|
@@ -149,6 +150,7 @@ var GP_E_NO_EVIDENCE = "GP_E_NO_EVIDENCE";
|
|
|
149
150
|
var GP_E_INTERNAL = "GP_E_INTERNAL";
|
|
150
151
|
var GP_E_PROJECT_LIMIT = "GP_E_PROJECT_LIMIT";
|
|
151
152
|
var GP_E_NOT_FOUND = "GP_E_NOT_FOUND";
|
|
153
|
+
var GP_E_REGISTRY_DRIFT = "GP_E_REGISTRY_DRIFT";
|
|
152
154
|
var GP_E_ENGINE_TIMEOUT = "GP_E_ENGINE_TIMEOUT";
|
|
153
155
|
var GP_E_PAYLOAD_TOO_LARGE = "GP_E_PAYLOAD_TOO_LARGE";
|
|
154
156
|
var GP_E_ENGINE_BACKLOG = "GP_E_ENGINE_BACKLOG";
|
|
@@ -223,6 +225,24 @@ function isReplayUnsafeMethod(method) {
|
|
|
223
225
|
return Object.prototype.hasOwnProperty.call(REPLAY_UNSAFE_REASONS, method);
|
|
224
226
|
}
|
|
225
227
|
var REPLAY_UNSAFE_METHOD_NAMES = Object.keys(REPLAY_UNSAFE_REASONS);
|
|
228
|
+
function describeCapabilityDrift(advertised, forwarded) {
|
|
229
|
+
if (advertised === null) {
|
|
230
|
+
return "engine self-report carries no capabilities list, so this shell cannot say which daemon methods it measured; every tool result in this session is unverified against the daemon's own method table (an engine older than the capability list, or a hello that was cut short)";
|
|
231
|
+
}
|
|
232
|
+
const missing = forwarded.filter((method) => !advertised.includes(method));
|
|
233
|
+
const unexposed = advertised.filter((method) => !forwarded.includes(method));
|
|
234
|
+
if (missing.length === 0 && unexposed.length === 0) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const parts = [];
|
|
238
|
+
if (missing.length > 0) {
|
|
239
|
+
parts.push(`this shell forwards ${missing.join(", ")} but the daemon's hello does not list it/them \u2014 that call will be answered methodNotFound, so the tool surface and the engine method table have drifted`);
|
|
240
|
+
}
|
|
241
|
+
if (unexposed.length > 0) {
|
|
242
|
+
parts.push(`the daemon offers ${unexposed.join(", ")} which this shell does not expose as a tool \u2014 not an error, but the agent cannot reach it from MCP`);
|
|
243
|
+
}
|
|
244
|
+
return `capability reconciliation: ${parts.join("; ")}`;
|
|
245
|
+
}
|
|
226
246
|
var STATE_CHANGING_ON_RETRY_METHOD_NAMES = ["snapshot"];
|
|
227
247
|
function engineDeadlineMs(method, params) {
|
|
228
248
|
if (method === "restore") {
|
|
@@ -307,12 +327,13 @@ function slowEngineRemedy(method, surface = "mcp") {
|
|
|
307
327
|
var LATE_REPLY_RETENTION = 16;
|
|
308
328
|
var LOG_BODY_CHARS = 64 * 1024;
|
|
309
329
|
var EngineJsonRpcClient = class {
|
|
310
|
-
constructor(io, timeoutMs, identity = null, callerCeilingMs = CALLER_VISIBLE_CEILING_MS, surface = "mcp") {
|
|
330
|
+
constructor(io, timeoutMs, identity = null, callerCeilingMs = CALLER_VISIBLE_CEILING_MS, surface = "mcp", expectedDaemonMethods = []) {
|
|
311
331
|
this.io = io;
|
|
312
332
|
this.timeoutMs = timeoutMs;
|
|
313
333
|
this.identity = identity;
|
|
314
334
|
this.callerCeilingMs = callerCeilingMs;
|
|
315
335
|
this.surface = surface;
|
|
336
|
+
this.expectedDaemonMethods = expectedDaemonMethods;
|
|
316
337
|
this.io.onMessage((line) => this.handleMessage(line));
|
|
317
338
|
this.io.onError((error) => {
|
|
318
339
|
if (error instanceof OversizeFrameError) {
|
|
@@ -337,6 +358,7 @@ var EngineJsonRpcClient = class {
|
|
|
337
358
|
identity;
|
|
338
359
|
callerCeilingMs;
|
|
339
360
|
surface;
|
|
361
|
+
expectedDaemonMethods;
|
|
340
362
|
/**
|
|
341
363
|
* Every mutable piece of this client's state, in one object that is *never*
|
|
342
364
|
* reassigned through `this`.
|
|
@@ -753,6 +775,13 @@ var EngineJsonRpcClient = class {
|
|
|
753
775
|
if (version !== null && wantVersion !== void 0 && version !== wantVersion) {
|
|
754
776
|
this.report(`version mismatch: engine self-reports '${version}', this MCP shell ships '${wantVersion}' (single version line) \u2014 one side is stale`);
|
|
755
777
|
}
|
|
778
|
+
if (this.expectedDaemonMethods.length > 0) {
|
|
779
|
+
const advertised = Array.isArray(report.capabilities) ? report.capabilities.filter((item) => typeof item === "string") : null;
|
|
780
|
+
const drift = describeCapabilityDrift(advertised, this.expectedDaemonMethods);
|
|
781
|
+
if (drift !== null) {
|
|
782
|
+
this.report(drift);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
756
785
|
}
|
|
757
786
|
};
|
|
758
787
|
function attribute(error, entry, inferred = true) {
|
|
@@ -847,7 +876,8 @@ function unixSocketEngineClient(socketPath, options = {}) {
|
|
|
847
876
|
void 0,
|
|
848
877
|
options.identity ?? null,
|
|
849
878
|
options.callerCeilingMs ?? CALLER_VISIBLE_CEILING_MS,
|
|
850
|
-
options.surface ?? "mcp"
|
|
879
|
+
options.surface ?? "mcp",
|
|
880
|
+
options.expectedDaemonMethods ?? []
|
|
851
881
|
);
|
|
852
882
|
}
|
|
853
883
|
|
|
@@ -1569,7 +1599,7 @@ function registryDriftReason(filePath, openedAs, now) {
|
|
|
1569
1599
|
function assertNoConcurrentWrite(filePath, openedAs) {
|
|
1570
1600
|
const reason = registryDriftReason(filePath, openedAs, fileIdentity(filePath));
|
|
1571
1601
|
if (reason !== null) {
|
|
1572
|
-
throw new ProjectRegistryError(
|
|
1602
|
+
throw new ProjectRegistryError(GP_E_REGISTRY_DRIFT, reason);
|
|
1573
1603
|
}
|
|
1574
1604
|
}
|
|
1575
1605
|
function saveProjects(filePath, entries) {
|
|
@@ -1887,7 +1917,7 @@ var ProjectRegistryError = class extends Error {
|
|
|
1887
1917
|
var PID_INT32_MAX = 2147483647;
|
|
1888
1918
|
var OptionalPidSchema = z5.number().int().min(1).max(PID_INT32_MAX).optional();
|
|
1889
1919
|
var OptionalDepthSchema = z5.number().int().min(1).max(10).optional();
|
|
1890
|
-
var OptionalRoleSchema = z5.string().max(SELECTOR_MAX_LENGTH).optional();
|
|
1920
|
+
var OptionalRoleSchema = z5.string().min(1).max(SELECTOR_MAX_LENGTH).optional();
|
|
1891
1921
|
var OptionalBundleIdSchema = z5.string().max(512).optional();
|
|
1892
1922
|
var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
1893
1923
|
var OptionalOperationIdSchema = z5.string().regex(OPERATION_ID_PATTERN2).optional();
|
|
@@ -2269,6 +2299,7 @@ var TOOL_SPECS = [
|
|
|
2269
2299
|
execute: projectGetTool
|
|
2270
2300
|
}
|
|
2271
2301
|
];
|
|
2302
|
+
var DAEMON_FORWARDED_METHODS = TOOL_SPECS.filter((spec) => spec.execute === void 0).map((spec) => spec.engineMethod);
|
|
2272
2303
|
var TOOL_BY_NAME = new Map(
|
|
2273
2304
|
TOOL_SPECS.map((spec) => [spec.name, spec])
|
|
2274
2305
|
);
|
|
@@ -2588,6 +2619,9 @@ function projectErrorRemedy(code) {
|
|
|
2588
2619
|
if (code === GP_E_NOT_FOUND) {
|
|
2589
2620
|
return "check the projectId; use gp_project_list to view available projects";
|
|
2590
2621
|
}
|
|
2622
|
+
if (code === GP_E_REGISTRY_DRIFT) {
|
|
2623
|
+
return "the projects file changed between the read that produced this reply and the write it attempted, so nothing was written: re-run the same gp_project_set \u2014 what this refusal wants is a second read, not different arguments. If it repeats, another writer owns that file (the daemon's maintenance CLI and the settings panel both rewrite it): read the table with `glasspaned --list-projects`, and remember a removal only sticks after the daemon restarts. `GLASSPANE_PROJECTS_FORCE_OVERWRITE=1` moves a file aside before writing, and nothing is overwritten without it.";
|
|
2624
|
+
}
|
|
2591
2625
|
if (code === GP_E_NO_USER_RECORD) {
|
|
2592
2626
|
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";
|
|
2593
2627
|
}
|
|
@@ -3433,6 +3467,8 @@ function httpStatusForEngineCode(code) {
|
|
|
3433
3467
|
return 503;
|
|
3434
3468
|
case GP_E_ENGINE_TIMEOUT:
|
|
3435
3469
|
return 504;
|
|
3470
|
+
case GP_E_REGISTRY_DRIFT:
|
|
3471
|
+
return 503;
|
|
3436
3472
|
case GP_E_PAYLOAD_TOO_LARGE:
|
|
3437
3473
|
return 502;
|
|
3438
3474
|
case GP_E_INTERNAL:
|
package/dist/http-gateway.js
CHANGED
|
@@ -141,6 +141,7 @@ var StreamLineIo = class {
|
|
|
141
141
|
this.input.destroy();
|
|
142
142
|
}
|
|
143
143
|
};
|
|
144
|
+
var STDIO_WRITE_BACKLOG_BYTES = 32 * 1024 * 1024;
|
|
144
145
|
|
|
145
146
|
// src/errors.ts
|
|
146
147
|
var GP_E_ENGINE_UNREACHABLE = "GP_E_ENGINE_UNREACHABLE";
|
|
@@ -149,6 +150,7 @@ var GP_E_NO_EVIDENCE = "GP_E_NO_EVIDENCE";
|
|
|
149
150
|
var GP_E_INTERNAL = "GP_E_INTERNAL";
|
|
150
151
|
var GP_E_PROJECT_LIMIT = "GP_E_PROJECT_LIMIT";
|
|
151
152
|
var GP_E_NOT_FOUND = "GP_E_NOT_FOUND";
|
|
153
|
+
var GP_E_REGISTRY_DRIFT = "GP_E_REGISTRY_DRIFT";
|
|
152
154
|
var GP_E_ENGINE_TIMEOUT = "GP_E_ENGINE_TIMEOUT";
|
|
153
155
|
var GP_E_PAYLOAD_TOO_LARGE = "GP_E_PAYLOAD_TOO_LARGE";
|
|
154
156
|
var GP_E_ENGINE_BACKLOG = "GP_E_ENGINE_BACKLOG";
|
|
@@ -219,6 +221,24 @@ function isReplayUnsafeMethod(method) {
|
|
|
219
221
|
return Object.prototype.hasOwnProperty.call(REPLAY_UNSAFE_REASONS, method);
|
|
220
222
|
}
|
|
221
223
|
var REPLAY_UNSAFE_METHOD_NAMES = Object.keys(REPLAY_UNSAFE_REASONS);
|
|
224
|
+
function describeCapabilityDrift(advertised, forwarded) {
|
|
225
|
+
if (advertised === null) {
|
|
226
|
+
return "engine self-report carries no capabilities list, so this shell cannot say which daemon methods it measured; every tool result in this session is unverified against the daemon's own method table (an engine older than the capability list, or a hello that was cut short)";
|
|
227
|
+
}
|
|
228
|
+
const missing = forwarded.filter((method) => !advertised.includes(method));
|
|
229
|
+
const unexposed = advertised.filter((method) => !forwarded.includes(method));
|
|
230
|
+
if (missing.length === 0 && unexposed.length === 0) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const parts = [];
|
|
234
|
+
if (missing.length > 0) {
|
|
235
|
+
parts.push(`this shell forwards ${missing.join(", ")} but the daemon's hello does not list it/them \u2014 that call will be answered methodNotFound, so the tool surface and the engine method table have drifted`);
|
|
236
|
+
}
|
|
237
|
+
if (unexposed.length > 0) {
|
|
238
|
+
parts.push(`the daemon offers ${unexposed.join(", ")} which this shell does not expose as a tool \u2014 not an error, but the agent cannot reach it from MCP`);
|
|
239
|
+
}
|
|
240
|
+
return `capability reconciliation: ${parts.join("; ")}`;
|
|
241
|
+
}
|
|
222
242
|
var STATE_CHANGING_ON_RETRY_METHOD_NAMES = ["snapshot"];
|
|
223
243
|
function engineDeadlineMs(method, params) {
|
|
224
244
|
if (method === "restore") {
|
|
@@ -303,12 +323,13 @@ function slowEngineRemedy(method, surface = "mcp") {
|
|
|
303
323
|
var LATE_REPLY_RETENTION = 16;
|
|
304
324
|
var LOG_BODY_CHARS = 64 * 1024;
|
|
305
325
|
var EngineJsonRpcClient = class {
|
|
306
|
-
constructor(io, timeoutMs, identity = null, callerCeilingMs = CALLER_VISIBLE_CEILING_MS, surface = "mcp") {
|
|
326
|
+
constructor(io, timeoutMs, identity = null, callerCeilingMs = CALLER_VISIBLE_CEILING_MS, surface = "mcp", expectedDaemonMethods = []) {
|
|
307
327
|
this.io = io;
|
|
308
328
|
this.timeoutMs = timeoutMs;
|
|
309
329
|
this.identity = identity;
|
|
310
330
|
this.callerCeilingMs = callerCeilingMs;
|
|
311
331
|
this.surface = surface;
|
|
332
|
+
this.expectedDaemonMethods = expectedDaemonMethods;
|
|
312
333
|
this.io.onMessage((line) => this.handleMessage(line));
|
|
313
334
|
this.io.onError((error) => {
|
|
314
335
|
if (error instanceof OversizeFrameError) {
|
|
@@ -333,6 +354,7 @@ var EngineJsonRpcClient = class {
|
|
|
333
354
|
identity;
|
|
334
355
|
callerCeilingMs;
|
|
335
356
|
surface;
|
|
357
|
+
expectedDaemonMethods;
|
|
336
358
|
/**
|
|
337
359
|
* Every mutable piece of this client's state, in one object that is *never*
|
|
338
360
|
* reassigned through `this`.
|
|
@@ -749,6 +771,13 @@ var EngineJsonRpcClient = class {
|
|
|
749
771
|
if (version !== null && wantVersion !== void 0 && version !== wantVersion) {
|
|
750
772
|
this.report(`version mismatch: engine self-reports '${version}', this MCP shell ships '${wantVersion}' (single version line) \u2014 one side is stale`);
|
|
751
773
|
}
|
|
774
|
+
if (this.expectedDaemonMethods.length > 0) {
|
|
775
|
+
const advertised = Array.isArray(report.capabilities) ? report.capabilities.filter((item) => typeof item === "string") : null;
|
|
776
|
+
const drift = describeCapabilityDrift(advertised, this.expectedDaemonMethods);
|
|
777
|
+
if (drift !== null) {
|
|
778
|
+
this.report(drift);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
752
781
|
}
|
|
753
782
|
};
|
|
754
783
|
function attribute(error, entry, inferred = true) {
|
|
@@ -843,7 +872,8 @@ function unixSocketEngineClient(socketPath, options = {}) {
|
|
|
843
872
|
void 0,
|
|
844
873
|
options.identity ?? null,
|
|
845
874
|
options.callerCeilingMs ?? CALLER_VISIBLE_CEILING_MS,
|
|
846
|
-
options.surface ?? "mcp"
|
|
875
|
+
options.surface ?? "mcp",
|
|
876
|
+
options.expectedDaemonMethods ?? []
|
|
847
877
|
);
|
|
848
878
|
}
|
|
849
879
|
|
|
@@ -1559,7 +1589,7 @@ function registryDriftReason(filePath, openedAs, now) {
|
|
|
1559
1589
|
function assertNoConcurrentWrite(filePath, openedAs) {
|
|
1560
1590
|
const reason = registryDriftReason(filePath, openedAs, fileIdentity(filePath));
|
|
1561
1591
|
if (reason !== null) {
|
|
1562
|
-
throw new ProjectRegistryError(
|
|
1592
|
+
throw new ProjectRegistryError(GP_E_REGISTRY_DRIFT, reason);
|
|
1563
1593
|
}
|
|
1564
1594
|
}
|
|
1565
1595
|
function saveProjects(filePath, entries) {
|
|
@@ -1877,7 +1907,7 @@ var ProjectRegistryError = class extends Error {
|
|
|
1877
1907
|
var PID_INT32_MAX = 2147483647;
|
|
1878
1908
|
var OptionalPidSchema = z5.number().int().min(1).max(PID_INT32_MAX).optional();
|
|
1879
1909
|
var OptionalDepthSchema = z5.number().int().min(1).max(10).optional();
|
|
1880
|
-
var OptionalRoleSchema = z5.string().max(SELECTOR_MAX_LENGTH).optional();
|
|
1910
|
+
var OptionalRoleSchema = z5.string().min(1).max(SELECTOR_MAX_LENGTH).optional();
|
|
1881
1911
|
var OptionalBundleIdSchema = z5.string().max(512).optional();
|
|
1882
1912
|
var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
1883
1913
|
var OptionalOperationIdSchema = z5.string().regex(OPERATION_ID_PATTERN2).optional();
|
|
@@ -2259,6 +2289,7 @@ var TOOL_SPECS = [
|
|
|
2259
2289
|
execute: projectGetTool
|
|
2260
2290
|
}
|
|
2261
2291
|
];
|
|
2292
|
+
var DAEMON_FORWARDED_METHODS = TOOL_SPECS.filter((spec) => spec.execute === void 0).map((spec) => spec.engineMethod);
|
|
2262
2293
|
var TOOL_BY_NAME = new Map(
|
|
2263
2294
|
TOOL_SPECS.map((spec) => [spec.name, spec])
|
|
2264
2295
|
);
|
|
@@ -2578,6 +2609,9 @@ function projectErrorRemedy(code) {
|
|
|
2578
2609
|
if (code === GP_E_NOT_FOUND) {
|
|
2579
2610
|
return "check the projectId; use gp_project_list to view available projects";
|
|
2580
2611
|
}
|
|
2612
|
+
if (code === GP_E_REGISTRY_DRIFT) {
|
|
2613
|
+
return "the projects file changed between the read that produced this reply and the write it attempted, so nothing was written: re-run the same gp_project_set \u2014 what this refusal wants is a second read, not different arguments. If it repeats, another writer owns that file (the daemon's maintenance CLI and the settings panel both rewrite it): read the table with `glasspaned --list-projects`, and remember a removal only sticks after the daemon restarts. `GLASSPANE_PROJECTS_FORCE_OVERWRITE=1` moves a file aside before writing, and nothing is overwritten without it.";
|
|
2614
|
+
}
|
|
2581
2615
|
if (code === GP_E_NO_USER_RECORD) {
|
|
2582
2616
|
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";
|
|
2583
2617
|
}
|
|
@@ -3423,6 +3457,8 @@ function httpStatusForEngineCode(code) {
|
|
|
3423
3457
|
return 503;
|
|
3424
3458
|
case GP_E_ENGINE_TIMEOUT:
|
|
3425
3459
|
return 504;
|
|
3460
|
+
case GP_E_REGISTRY_DRIFT:
|
|
3461
|
+
return 503;
|
|
3426
3462
|
case GP_E_PAYLOAD_TOO_LARGE:
|
|
3427
3463
|
return 502;
|
|
3428
3464
|
case GP_E_INTERNAL:
|
package/dist/index.js
CHANGED
|
@@ -495,6 +495,30 @@ function readUpdateState(env = process.env, stateRootOverride) {
|
|
|
495
495
|
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`
|
|
496
496
|
};
|
|
497
497
|
}
|
|
498
|
+
let stat;
|
|
499
|
+
try {
|
|
500
|
+
stat = fs.statSync(stateFile);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
return {
|
|
503
|
+
kind: "unreadable",
|
|
504
|
+
stateFile,
|
|
505
|
+
summary: `update state: ${stateFile} exists but could not be stat-ed (${String(error)}); freshness is unknown, which is not the same as up to date`
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
if (!stat.isFile()) {
|
|
509
|
+
return {
|
|
510
|
+
kind: "unreadable",
|
|
511
|
+
stateFile,
|
|
512
|
+
summary: `update state: ${stateFile} is not a regular file (mode ${(stat.mode & 511).toString(8)}), so nothing was parsed from it; freshness is unknown, which is not the same as up to date`
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if ((stat.mode & 63) !== 0) {
|
|
516
|
+
return {
|
|
517
|
+
kind: "unreadable",
|
|
518
|
+
stateFile,
|
|
519
|
+
summary: `update state: ${stateFile} is mode ${(stat.mode & 511).toString(8)} \u2014 group/world readable, so this reader will not present it as the owner-only document it claims to be. Run \`chmod 600 ${stateFile}\` (the updater writes it 0600) and read again; freshness stays unknown, which is not the same as up to date`
|
|
520
|
+
};
|
|
521
|
+
}
|
|
498
522
|
let text;
|
|
499
523
|
try {
|
|
500
524
|
text = fs.readFileSync(stateFile, "utf8");
|
|
@@ -729,12 +753,14 @@ var OrderedReplyQueue = class {
|
|
|
729
753
|
return this.tail;
|
|
730
754
|
}
|
|
731
755
|
};
|
|
756
|
+
var STDIO_WRITE_BACKLOG_BYTES = 32 * 1024 * 1024;
|
|
732
757
|
var DrainAwareWriter = class {
|
|
733
|
-
constructor(sink, report, drainWaitMs, onDead) {
|
|
758
|
+
constructor(sink, report, drainWaitMs, onDead, backlogLimitBytes = STDIO_WRITE_BACKLOG_BYTES) {
|
|
734
759
|
this.sink = sink;
|
|
735
760
|
this.report = report;
|
|
736
761
|
this.drainWaitMs = drainWaitMs;
|
|
737
762
|
this.onDead = onDead;
|
|
763
|
+
this.backlogLimitBytes = backlogLimitBytes;
|
|
738
764
|
sink.on("error", (error) => {
|
|
739
765
|
if (this.dead) {
|
|
740
766
|
return;
|
|
@@ -748,15 +774,27 @@ var DrainAwareWriter = class {
|
|
|
748
774
|
report;
|
|
749
775
|
drainWaitMs;
|
|
750
776
|
onDead;
|
|
777
|
+
backlogLimitBytes;
|
|
751
778
|
dead = null;
|
|
779
|
+
dropped = 0;
|
|
752
780
|
get failed() {
|
|
753
781
|
return this.dead !== null;
|
|
754
782
|
}
|
|
783
|
+
/** How many replies this writer refused to queue. `gp_diagnose` reads it: a
|
|
784
|
+
* silently-shrinking answer is the failure mode this ceiling replaces. */
|
|
785
|
+
get droppedFrames() {
|
|
786
|
+
return this.dropped;
|
|
787
|
+
}
|
|
755
788
|
/** @param text already framed, including its trailing newline. */
|
|
756
789
|
async write(text) {
|
|
757
790
|
if (this.failed) {
|
|
758
791
|
return false;
|
|
759
792
|
}
|
|
793
|
+
if (this.sink.writableLength >= this.backlogLimitBytes) {
|
|
794
|
+
this.dropped += 1;
|
|
795
|
+
this.report(`reply dropped, not queued: stdout already holds ${this.sink.writableLength} bytes past the ${this.backlogLimitBytes} ceiling \u2014 the reader on the other end stopped consuming, so this frame (and ${this.dropped - 1} earlier one(s)) will not be delivered`);
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
760
798
|
let flowing = true;
|
|
761
799
|
try {
|
|
762
800
|
flowing = this.sink.write(text);
|
|
@@ -809,6 +847,7 @@ var GP_E_NO_EVIDENCE = "GP_E_NO_EVIDENCE";
|
|
|
809
847
|
var GP_E_INTERNAL = "GP_E_INTERNAL";
|
|
810
848
|
var GP_E_PROJECT_LIMIT = "GP_E_PROJECT_LIMIT";
|
|
811
849
|
var GP_E_NOT_FOUND = "GP_E_NOT_FOUND";
|
|
850
|
+
var GP_E_REGISTRY_DRIFT = "GP_E_REGISTRY_DRIFT";
|
|
812
851
|
var GP_E_ENGINE_TIMEOUT = "GP_E_ENGINE_TIMEOUT";
|
|
813
852
|
var GP_E_PAYLOAD_TOO_LARGE = "GP_E_PAYLOAD_TOO_LARGE";
|
|
814
853
|
var GP_E_ENGINE_BACKLOG = "GP_E_ENGINE_BACKLOG";
|
|
@@ -883,6 +922,24 @@ function isReplayUnsafeMethod(method) {
|
|
|
883
922
|
return Object.prototype.hasOwnProperty.call(REPLAY_UNSAFE_REASONS, method);
|
|
884
923
|
}
|
|
885
924
|
var REPLAY_UNSAFE_METHOD_NAMES = Object.keys(REPLAY_UNSAFE_REASONS);
|
|
925
|
+
function describeCapabilityDrift(advertised, forwarded) {
|
|
926
|
+
if (advertised === null) {
|
|
927
|
+
return "engine self-report carries no capabilities list, so this shell cannot say which daemon methods it measured; every tool result in this session is unverified against the daemon's own method table (an engine older than the capability list, or a hello that was cut short)";
|
|
928
|
+
}
|
|
929
|
+
const missing = forwarded.filter((method) => !advertised.includes(method));
|
|
930
|
+
const unexposed = advertised.filter((method) => !forwarded.includes(method));
|
|
931
|
+
if (missing.length === 0 && unexposed.length === 0) {
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
const parts = [];
|
|
935
|
+
if (missing.length > 0) {
|
|
936
|
+
parts.push(`this shell forwards ${missing.join(", ")} but the daemon's hello does not list it/them \u2014 that call will be answered methodNotFound, so the tool surface and the engine method table have drifted`);
|
|
937
|
+
}
|
|
938
|
+
if (unexposed.length > 0) {
|
|
939
|
+
parts.push(`the daemon offers ${unexposed.join(", ")} which this shell does not expose as a tool \u2014 not an error, but the agent cannot reach it from MCP`);
|
|
940
|
+
}
|
|
941
|
+
return `capability reconciliation: ${parts.join("; ")}`;
|
|
942
|
+
}
|
|
886
943
|
var STATE_CHANGING_ON_RETRY_METHOD_NAMES = ["snapshot"];
|
|
887
944
|
function engineDeadlineMs(method, params) {
|
|
888
945
|
if (method === "restore") {
|
|
@@ -967,12 +1024,13 @@ function slowEngineRemedy(method, surface = "mcp") {
|
|
|
967
1024
|
var LATE_REPLY_RETENTION = 16;
|
|
968
1025
|
var LOG_BODY_CHARS = 64 * 1024;
|
|
969
1026
|
var EngineJsonRpcClient = class {
|
|
970
|
-
constructor(io, timeoutMs, identity = null, callerCeilingMs = CALLER_VISIBLE_CEILING_MS, surface = "mcp") {
|
|
1027
|
+
constructor(io, timeoutMs, identity = null, callerCeilingMs = CALLER_VISIBLE_CEILING_MS, surface = "mcp", expectedDaemonMethods = []) {
|
|
971
1028
|
this.io = io;
|
|
972
1029
|
this.timeoutMs = timeoutMs;
|
|
973
1030
|
this.identity = identity;
|
|
974
1031
|
this.callerCeilingMs = callerCeilingMs;
|
|
975
1032
|
this.surface = surface;
|
|
1033
|
+
this.expectedDaemonMethods = expectedDaemonMethods;
|
|
976
1034
|
this.io.onMessage((line) => this.handleMessage(line));
|
|
977
1035
|
this.io.onError((error) => {
|
|
978
1036
|
if (error instanceof OversizeFrameError) {
|
|
@@ -997,6 +1055,7 @@ var EngineJsonRpcClient = class {
|
|
|
997
1055
|
identity;
|
|
998
1056
|
callerCeilingMs;
|
|
999
1057
|
surface;
|
|
1058
|
+
expectedDaemonMethods;
|
|
1000
1059
|
/**
|
|
1001
1060
|
* Every mutable piece of this client's state, in one object that is *never*
|
|
1002
1061
|
* reassigned through `this`.
|
|
@@ -1413,6 +1472,13 @@ var EngineJsonRpcClient = class {
|
|
|
1413
1472
|
if (version !== null && wantVersion !== void 0 && version !== wantVersion) {
|
|
1414
1473
|
this.report(`version mismatch: engine self-reports '${version}', this MCP shell ships '${wantVersion}' (single version line) \u2014 one side is stale`);
|
|
1415
1474
|
}
|
|
1475
|
+
if (this.expectedDaemonMethods.length > 0) {
|
|
1476
|
+
const advertised = Array.isArray(report.capabilities) ? report.capabilities.filter((item) => typeof item === "string") : null;
|
|
1477
|
+
const drift = describeCapabilityDrift(advertised, this.expectedDaemonMethods);
|
|
1478
|
+
if (drift !== null) {
|
|
1479
|
+
this.report(drift);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1416
1482
|
}
|
|
1417
1483
|
};
|
|
1418
1484
|
function attribute(error, entry, inferred = true) {
|
|
@@ -1507,7 +1573,8 @@ function unixSocketEngineClient(socketPath, options = {}) {
|
|
|
1507
1573
|
void 0,
|
|
1508
1574
|
options.identity ?? null,
|
|
1509
1575
|
options.callerCeilingMs ?? CALLER_VISIBLE_CEILING_MS,
|
|
1510
|
-
options.surface ?? "mcp"
|
|
1576
|
+
options.surface ?? "mcp",
|
|
1577
|
+
options.expectedDaemonMethods ?? []
|
|
1511
1578
|
);
|
|
1512
1579
|
}
|
|
1513
1580
|
|
|
@@ -1945,7 +2012,7 @@ function registryDriftReason(filePath, openedAs, now) {
|
|
|
1945
2012
|
function assertNoConcurrentWrite(filePath, openedAs) {
|
|
1946
2013
|
const reason = registryDriftReason(filePath, openedAs, fileIdentity(filePath));
|
|
1947
2014
|
if (reason !== null) {
|
|
1948
|
-
throw new ProjectRegistryError(
|
|
2015
|
+
throw new ProjectRegistryError(GP_E_REGISTRY_DRIFT, reason);
|
|
1949
2016
|
}
|
|
1950
2017
|
}
|
|
1951
2018
|
function saveProjects(filePath, entries) {
|
|
@@ -2263,7 +2330,7 @@ var ProjectRegistryError = class extends Error {
|
|
|
2263
2330
|
var PID_INT32_MAX = 2147483647;
|
|
2264
2331
|
var OptionalPidSchema = z5.number().int().min(1).max(PID_INT32_MAX).optional();
|
|
2265
2332
|
var OptionalDepthSchema = z5.number().int().min(1).max(10).optional();
|
|
2266
|
-
var OptionalRoleSchema = z5.string().max(SELECTOR_MAX_LENGTH).optional();
|
|
2333
|
+
var OptionalRoleSchema = z5.string().min(1).max(SELECTOR_MAX_LENGTH).optional();
|
|
2267
2334
|
var OptionalBundleIdSchema = z5.string().max(512).optional();
|
|
2268
2335
|
var OPERATION_ID_PATTERN2 = /^op_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
2269
2336
|
var OptionalOperationIdSchema = z5.string().regex(OPERATION_ID_PATTERN2).optional();
|
|
@@ -2645,6 +2712,7 @@ var TOOL_SPECS = [
|
|
|
2645
2712
|
execute: projectGetTool
|
|
2646
2713
|
}
|
|
2647
2714
|
];
|
|
2715
|
+
var DAEMON_FORWARDED_METHODS = TOOL_SPECS.filter((spec) => spec.execute === void 0).map((spec) => spec.engineMethod);
|
|
2648
2716
|
var TOOL_BY_NAME = new Map(
|
|
2649
2717
|
TOOL_SPECS.map((spec) => [spec.name, spec])
|
|
2650
2718
|
);
|
|
@@ -3209,6 +3277,9 @@ function projectErrorRemedy(code) {
|
|
|
3209
3277
|
if (code === GP_E_NOT_FOUND) {
|
|
3210
3278
|
return "check the projectId; use gp_project_list to view available projects";
|
|
3211
3279
|
}
|
|
3280
|
+
if (code === GP_E_REGISTRY_DRIFT) {
|
|
3281
|
+
return "the projects file changed between the read that produced this reply and the write it attempted, so nothing was written: re-run the same gp_project_set \u2014 what this refusal wants is a second read, not different arguments. If it repeats, another writer owns that file (the daemon's maintenance CLI and the settings panel both rewrite it): read the table with `glasspaned --list-projects`, and remember a removal only sticks after the daemon restarts. `GLASSPANE_PROJECTS_FORCE_OVERWRITE=1` moves a file aside before writing, and nothing is overwritten without it.";
|
|
3282
|
+
}
|
|
3212
3283
|
if (code === GP_E_NO_USER_RECORD) {
|
|
3213
3284
|
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";
|
|
3214
3285
|
}
|
|
@@ -3722,7 +3793,7 @@ var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
|
3722
3793
|
"2025-03-26",
|
|
3723
3794
|
MCP_PROTOCOL_VERSION
|
|
3724
3795
|
];
|
|
3725
|
-
var SERVER_INFO = { name: "glasspane-mcp", version: "1.
|
|
3796
|
+
var SERVER_INFO = { name: "glasspane-mcp", version: "1.13.0" };
|
|
3726
3797
|
var PARSE_ERROR = -32700;
|
|
3727
3798
|
var INVALID_REQUEST = -32600;
|
|
3728
3799
|
var METHOD_NOT_FOUND = -32601;
|
|
@@ -4016,7 +4087,10 @@ function main() {
|
|
|
4016
4087
|
let engine;
|
|
4017
4088
|
try {
|
|
4018
4089
|
engine = unixSocketEngineClient(options.socketPath, {
|
|
4019
|
-
identity: { version: SERVER_INFO.version }
|
|
4090
|
+
identity: { version: SERVER_INFO.version },
|
|
4091
|
+
// `hello.capabilities` 的读者在这一层接线:清单由 daemon 派生、由这里与 TOOL_SPECS
|
|
4092
|
+
// 对账,两边谁漂了都会有人说,而不是各自安静地活着。
|
|
4093
|
+
expectedDaemonMethods: DAEMON_FORWARDED_METHODS
|
|
4020
4094
|
});
|
|
4021
4095
|
} catch (error) {
|
|
4022
4096
|
writeDescriptor(
|
package/dist/io.d.ts
CHANGED
|
@@ -131,6 +131,15 @@ export declare class OrderedReplyQueue {
|
|
|
131
131
|
/** Resolves once everything queued so far has been attempted. */
|
|
132
132
|
drained(): Promise<void>;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* 回包在 stdout 队列里允许的积压上限。镜像方向的同一件事早就有闸(`engine-client.ts` 的
|
|
136
|
+
* `ENGINE_WRITE_BACKLOG_BYTES` + 自己的错误码),stdio 这一侧却只说过"有 DrainAwareWriter
|
|
137
|
+
* 管背压"——串行化不等于**有界**:30s 的 drain 看门狗一响,后面的 `write()` 照样继续往
|
|
138
|
+
* Node 的队列里塞,一个不读了客户端就能把 `gp_recent_reports`(20 × 4 MiB,且这层没有
|
|
139
|
+
* 出站帧长上限)变成无界内存。到线就丢这一帧、点名丢了谁、说清为什么,比让进程被 OOM
|
|
140
|
+
* 杀掉后再也答不出第 21 条更好。
|
|
141
|
+
*/
|
|
142
|
+
export declare const STDIO_WRITE_BACKLOG_BYTES: number;
|
|
134
143
|
/**
|
|
135
144
|
* Writes with backpressure honoured and a dead pipe survived: an ignored
|
|
136
145
|
* `write()` return lets replies pile up in memory, and an unlistened `error`
|
|
@@ -141,9 +150,14 @@ export declare class DrainAwareWriter {
|
|
|
141
150
|
private readonly report;
|
|
142
151
|
private readonly drainWaitMs;
|
|
143
152
|
private readonly onDead;
|
|
153
|
+
private readonly backlogLimitBytes;
|
|
144
154
|
private dead;
|
|
145
|
-
|
|
155
|
+
private dropped;
|
|
156
|
+
constructor(sink: Writable, report: (note: string) => void, drainWaitMs: number, onDead: (error: Error) => void, backlogLimitBytes?: number);
|
|
146
157
|
get failed(): boolean;
|
|
158
|
+
/** How many replies this writer refused to queue. `gp_diagnose` reads it: a
|
|
159
|
+
* silently-shrinking answer is the failure mode this ceiling replaces. */
|
|
160
|
+
get droppedFrames(): number;
|
|
147
161
|
/** @param text already framed, including its trailing newline. */
|
|
148
162
|
write(text: string): Promise<boolean>;
|
|
149
163
|
private waitForDrain;
|
package/dist/tools.d.ts
CHANGED
|
@@ -288,6 +288,14 @@ export interface ToolResult {
|
|
|
288
288
|
isError: boolean;
|
|
289
289
|
}
|
|
290
290
|
export declare const TOOL_SPECS: readonly ToolSpec[];
|
|
291
|
+
/**
|
|
292
|
+
* The daemon methods this shell forwards, derived from {@link TOOL_SPECS} rather
|
|
293
|
+
* than written out. This is the list `hello.capabilities` is reconciled against
|
|
294
|
+
* (`engine-client.describeCapabilityDrift`): a hand-copied second truth is the
|
|
295
|
+
* exact drift the reader exists to catch, and the gateway already derives its own
|
|
296
|
+
* forwardable set from `execute === undefined` for the same reason.
|
|
297
|
+
*/
|
|
298
|
+
export declare const DAEMON_FORWARDED_METHODS: readonly string[];
|
|
291
299
|
export declare const TOOL_BY_NAME: ReadonlyMap<string, ToolSpec>;
|
|
292
300
|
/** Does this tool read or write `EvidenceAuditSession`? */
|
|
293
301
|
export declare function trailScopedTool(spec: ToolSpec): boolean;
|
package/package.json
CHANGED