glasspane-mcp 1.14.0 → 1.15.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 +9 -2
- package/dist/http-gateway-cli.js +20 -7
- package/dist/http-gateway.js +20 -7
- package/dist/index.js +62 -24
- package/dist/io.d.ts +12 -4
- package/dist/project-registry.d.ts +2 -1
- package/dist/tools.d.ts +34 -1
- package/package.json +1 -1
- package/schemas/convergence-round-state.schema.json +37 -0
- package/schemas/decision-log-entry.schema.json +20 -0
- package/schemas/evidence-pack.schema.json +6 -3
- package/schemas/invariant-report.schema.json +108 -0
- package/schemas/provider-registration.schema.json +63 -0
- package/schemas/reverification-request.schema.json +89 -0
- package/schemas/run-plan.schema.json +60 -0
package/dist/dispatch.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export declare const MCP_PROTOCOL_VERSION: "2025-06-18";
|
|
|
26
26
|
export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
|
|
27
27
|
export declare const SERVER_INFO: {
|
|
28
28
|
readonly name: "glasspane-mcp";
|
|
29
|
-
readonly version: "1.
|
|
29
|
+
readonly version: "1.15.0";
|
|
30
30
|
};
|
|
31
31
|
export declare const PARSE_ERROR = -32700;
|
|
32
32
|
export declare const INVALID_REQUEST = -32600;
|
|
@@ -63,6 +63,13 @@ export interface McpServerDeps {
|
|
|
63
63
|
* request it refers to stayed in flight and kept acting on the user's screen.
|
|
64
64
|
*/
|
|
65
65
|
report?: (note: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Reads how many replies this shell's stdout writer dropped because the pipe
|
|
68
|
+
* stopped draining. `gp_diagnose` surfaces it so a dropped reply is visible
|
|
69
|
+
* in-band rather than only in the log channel; it is wired from `index.ts`,
|
|
70
|
+
* where the `DrainAwareWriter` that counts them lives.
|
|
71
|
+
*/
|
|
72
|
+
stdoutDroppedFrames?: () => number;
|
|
66
73
|
}
|
|
67
74
|
export declare class McpServer {
|
|
68
75
|
private readonly deps;
|
|
@@ -103,7 +110,7 @@ export declare class McpServer {
|
|
|
103
110
|
* that back from is this trail (`gp_recent_reports`). A sink that recorded into
|
|
104
111
|
* a second session would look fixed and change nothing.
|
|
105
112
|
*/
|
|
106
|
-
export declare function createTrackedMcpServer(engine: EngineJsonRpcClient, report: (note: string) => void): {
|
|
113
|
+
export declare function createTrackedMcpServer(engine: EngineJsonRpcClient, report: (note: string) => void, stdoutDroppedFrames?: () => number): {
|
|
107
114
|
server: McpServer;
|
|
108
115
|
session: EvidenceAuditSession;
|
|
109
116
|
};
|
package/dist/http-gateway-cli.js
CHANGED
|
@@ -1730,7 +1730,7 @@ function daemonRestartNotice(filePath) {
|
|
|
1730
1730
|
return `Saved in ${filePath}, the projects file under the home record this user's state root defaults to. If this is the root the service was started on \u2014 which is not knowable from here \u2014 then the running background service loaded that file once when it started, so gp_attach will keep returning GP_E_NOT_FOUND for this projectId until the service restarts: ${restartCommand()}. ${rootDiscriminator()}`;
|
|
1731
1731
|
}
|
|
1732
1732
|
function rootDiscriminator() {
|
|
1733
|
-
return
|
|
1733
|
+
return 'Tell the two cases apart with the service\'s own answer: gp_project_list reads the file this call wrote, so an entry it lists is on disk, and gp_attach {projectId} is answered by the running service \u2014 an entry the list shows plus an attach that answers GP_E_NOT_FOUND means the service is loading a different projects.json than the one this write reached, not that the write failed. Which file the running service loads is not something this shell guesses from $HOME: `glasspaned --state-dir <root>` moves it to <root>/projects.json, and the daemon reports the root it resolved \u2014 `hello` names the state root under its `stateRoot` key. A daemon predating 1.15.0 omits the key, and an omitted key means "that daemon does not report it", never "so it must be the home default": for those binaries the discriminator below is the only read. Cross-check either way \u2014 `launchctl print gui/' + currentUid() + "/com.glasspane.daemon` prints the arguments the job was started with. If those arguments name a --state-dir, the entry has to go into that root's projects.json (repeat gp_project_set with " + PROJECTS_FILE_ENV + " pointed at <that root>/projects.json): no restart of the service will make it read anywhere else, so restarting again changes nothing. If they name no --state-dir, the service is on the default root and simply predates this write, and one restart with the command in this notice is what surfaces it.";
|
|
1734
1734
|
}
|
|
1735
1735
|
function restartCommand() {
|
|
1736
1736
|
return `run \`launchctl kickstart -k gui/${currentUid()}/com.glasspane.daemon\` \u2014 if that reports the service was not found, the launchd job was never bootstrapped, so ${daemonUnreachableRemedy()}`;
|
|
@@ -2054,7 +2054,7 @@ var TOOL_SPECS = [
|
|
|
2054
2054
|
additionalProperties: false,
|
|
2055
2055
|
properties: {
|
|
2056
2056
|
maxDepth: { type: "integer", minimum: 1, maximum: 10 },
|
|
2057
|
-
role: { type: "string", maxLength: SELECTOR_MAX_LENGTH }
|
|
2057
|
+
role: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH }
|
|
2058
2058
|
}
|
|
2059
2059
|
},
|
|
2060
2060
|
validate: zodBridge(ObserveArgs)
|
|
@@ -2120,7 +2120,8 @@ var TOOL_SPECS = [
|
|
|
2120
2120
|
}
|
|
2121
2121
|
},
|
|
2122
2122
|
validate: zodBridge(CaptureViewArgs),
|
|
2123
|
-
execute: captureView
|
|
2123
|
+
execute: captureView,
|
|
2124
|
+
reachesDaemon: true
|
|
2124
2125
|
},
|
|
2125
2126
|
{
|
|
2126
2127
|
name: "gp_diagnose",
|
|
@@ -2299,7 +2300,7 @@ var TOOL_SPECS = [
|
|
|
2299
2300
|
execute: projectGetTool
|
|
2300
2301
|
}
|
|
2301
2302
|
];
|
|
2302
|
-
var DAEMON_FORWARDED_METHODS = TOOL_SPECS.filter((spec) => spec.execute === void 0).map((spec) => spec.engineMethod);
|
|
2303
|
+
var DAEMON_FORWARDED_METHODS = TOOL_SPECS.filter((spec) => spec.execute === void 0 || spec.reachesDaemon === true).map((spec) => spec.engineMethod);
|
|
2303
2304
|
var TOOL_BY_NAME = new Map(
|
|
2304
2305
|
TOOL_SPECS.map((spec) => [spec.name, spec])
|
|
2305
2306
|
);
|
|
@@ -2699,6 +2700,10 @@ function renderTrailId(id, format) {
|
|
|
2699
2700
|
function renderTrailIds(ids, format) {
|
|
2700
2701
|
return ids.map((id) => renderTrailId(id, format)).join(", ");
|
|
2701
2702
|
}
|
|
2703
|
+
function renderErrorLine(text, format) {
|
|
2704
|
+
const stripped = text.replace(/[\u0000-\u001f\u007f]/g, "\xB7");
|
|
2705
|
+
return format === "html" ? escapeHTML(stripped) : stripped.replace(/[`[\]<>#*\\]/g, "\xB7");
|
|
2706
|
+
}
|
|
2702
2707
|
function describeFetchFailure(error) {
|
|
2703
2708
|
if (error instanceof EngineCallError) {
|
|
2704
2709
|
return formatToolErrorShape(error.toBody());
|
|
@@ -2708,7 +2713,11 @@ function describeFetchFailure(error) {
|
|
|
2708
2713
|
const [first] = mapped.content;
|
|
2709
2714
|
return first !== void 0 && first.type === "text" ? first.text : "the evidence read failed";
|
|
2710
2715
|
}
|
|
2711
|
-
return
|
|
2716
|
+
return formatToolError(
|
|
2717
|
+
GP_E_INTERNAL,
|
|
2718
|
+
String(error),
|
|
2719
|
+
"a fetch inside gp_recent_reports failed for a reason that is neither a daemon error frame nor an evidence-shape refusal, so this shell cannot name the cause from here. Read that operation on its own with gp_last_evidence {operationId} \u2014 it answers inside its own deadline and reports the fault by itself \u2014 or ask again with a smaller limit. Do not re-run gp_act to regenerate what the trail already holds"
|
|
2720
|
+
);
|
|
2712
2721
|
}
|
|
2713
2722
|
async function recentReports(args, context) {
|
|
2714
2723
|
const argv = args;
|
|
@@ -2791,7 +2800,11 @@ async function recentReports(args, context) {
|
|
|
2791
2800
|
}
|
|
2792
2801
|
if (packs.length === 0 && hardFailure !== null) {
|
|
2793
2802
|
const [first] = hardFailure.mapped.content;
|
|
2794
|
-
const base = first !== void 0 && first.type === "text" ? first.text :
|
|
2803
|
+
const base = first !== void 0 && first.type === "text" ? first.text : formatToolError(
|
|
2804
|
+
GP_E_INTERNAL,
|
|
2805
|
+
"the evidence read failed",
|
|
2806
|
+
"the fetch mapped to an answer this shell could not render as text, so no more than the failure itself is known here. Read the operation on its own with gp_last_evidence {operationId}, which reports its own fault with a remedy, or ask again with a smaller limit. Do not re-run gp_act to regenerate what the trail already holds"
|
|
2807
|
+
);
|
|
2795
2808
|
const extras = [];
|
|
2796
2809
|
if (notFetched.length > 0) {
|
|
2797
2810
|
extras.push(`${notFetched.length} further ${notFetched.length === 1 ? "entry" : "entries"} never fetched: ` + renderTrailIds(notFetched, format));
|
|
@@ -2834,7 +2847,7 @@ async function recentReports(args, context) {
|
|
|
2834
2847
|
const skipNote = skipped.length === 0 ? "" : note("gp-skipped", `skipped ${skipped.length} unreachable ${skipped.length === 1 ? "entry" : "entries"}: ${renderTrailIds(skipped, format)}`);
|
|
2835
2848
|
const unfetchedNote = notFetched.length === 0 ? "" : format === "html" ? `<p class="gp-not-fetched">not fetched within this report's ${RECENT_REPORTS_BUDGET_MS}ms wait: ${notFetched.length} ${notFetched.length === 1 ? "entry" : "entries"} left unanswered: ${renderTrailIds(notFetched, format)}. Fetch one by id with gp_last_evidence, or ask again with a smaller limit</p>` : `> not fetched within this report's ${RECENT_REPORTS_BUDGET_MS}ms wait: ${notFetched.length} ${notFetched.length === 1 ? "entry" : "entries"} left unanswered: ${renderTrailIds(notFetched, format)}. Fetch one by id with gp_last_evidence, or ask again with a smaller limit`;
|
|
2836
2849
|
const rejectNote = rejected.length === 0 ? "" : note("gp-rejected", `${rejectedClause(rejected.length)}${renderTrailIds(rejected, format)}. They were not sent, so nothing was lost by their absence here: gp_last_evidence validates an id the same way before it asks`);
|
|
2837
|
-
const failureNote = hardFailure === null ? "" : note("gp-fetch-failed", `the report stops here: ${renderTrailId(hardFailure.id, format)} failed with ${
|
|
2850
|
+
const failureNote = hardFailure === null ? "" : note("gp-fetch-failed", `the report stops here: ${renderTrailId(hardFailure.id, format)} failed with ${renderErrorLine(hardFailure.line, format)}. The ${packs.length} ${packs.length === 1 ? "pack" : "packs"} above were fetched and are rendered; read the failed one on its own with gp_last_evidence {operationId}. Do not re-run gp_act to regenerate what the trail already holds`);
|
|
2838
2851
|
const separator = format === "html" ? "\n<hr>\n" : "\n\n---\n";
|
|
2839
2852
|
const body = packs.map(({ pack }) => render2(pack, void 0)).join(separator);
|
|
2840
2853
|
const sections2 = [header];
|
package/dist/http-gateway.js
CHANGED
|
@@ -1720,7 +1720,7 @@ function daemonRestartNotice(filePath) {
|
|
|
1720
1720
|
return `Saved in ${filePath}, the projects file under the home record this user's state root defaults to. If this is the root the service was started on \u2014 which is not knowable from here \u2014 then the running background service loaded that file once when it started, so gp_attach will keep returning GP_E_NOT_FOUND for this projectId until the service restarts: ${restartCommand()}. ${rootDiscriminator()}`;
|
|
1721
1721
|
}
|
|
1722
1722
|
function rootDiscriminator() {
|
|
1723
|
-
return
|
|
1723
|
+
return 'Tell the two cases apart with the service\'s own answer: gp_project_list reads the file this call wrote, so an entry it lists is on disk, and gp_attach {projectId} is answered by the running service \u2014 an entry the list shows plus an attach that answers GP_E_NOT_FOUND means the service is loading a different projects.json than the one this write reached, not that the write failed. Which file the running service loads is not something this shell guesses from $HOME: `glasspaned --state-dir <root>` moves it to <root>/projects.json, and the daemon reports the root it resolved \u2014 `hello` names the state root under its `stateRoot` key. A daemon predating 1.15.0 omits the key, and an omitted key means "that daemon does not report it", never "so it must be the home default": for those binaries the discriminator below is the only read. Cross-check either way \u2014 `launchctl print gui/' + currentUid() + "/com.glasspane.daemon` prints the arguments the job was started with. If those arguments name a --state-dir, the entry has to go into that root's projects.json (repeat gp_project_set with " + PROJECTS_FILE_ENV + " pointed at <that root>/projects.json): no restart of the service will make it read anywhere else, so restarting again changes nothing. If they name no --state-dir, the service is on the default root and simply predates this write, and one restart with the command in this notice is what surfaces it.";
|
|
1724
1724
|
}
|
|
1725
1725
|
function restartCommand() {
|
|
1726
1726
|
return `run \`launchctl kickstart -k gui/${currentUid()}/com.glasspane.daemon\` \u2014 if that reports the service was not found, the launchd job was never bootstrapped, so ${daemonUnreachableRemedy()}`;
|
|
@@ -2044,7 +2044,7 @@ var TOOL_SPECS = [
|
|
|
2044
2044
|
additionalProperties: false,
|
|
2045
2045
|
properties: {
|
|
2046
2046
|
maxDepth: { type: "integer", minimum: 1, maximum: 10 },
|
|
2047
|
-
role: { type: "string", maxLength: SELECTOR_MAX_LENGTH }
|
|
2047
|
+
role: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH }
|
|
2048
2048
|
}
|
|
2049
2049
|
},
|
|
2050
2050
|
validate: zodBridge(ObserveArgs)
|
|
@@ -2110,7 +2110,8 @@ var TOOL_SPECS = [
|
|
|
2110
2110
|
}
|
|
2111
2111
|
},
|
|
2112
2112
|
validate: zodBridge(CaptureViewArgs),
|
|
2113
|
-
execute: captureView
|
|
2113
|
+
execute: captureView,
|
|
2114
|
+
reachesDaemon: true
|
|
2114
2115
|
},
|
|
2115
2116
|
{
|
|
2116
2117
|
name: "gp_diagnose",
|
|
@@ -2289,7 +2290,7 @@ var TOOL_SPECS = [
|
|
|
2289
2290
|
execute: projectGetTool
|
|
2290
2291
|
}
|
|
2291
2292
|
];
|
|
2292
|
-
var DAEMON_FORWARDED_METHODS = TOOL_SPECS.filter((spec) => spec.execute === void 0).map((spec) => spec.engineMethod);
|
|
2293
|
+
var DAEMON_FORWARDED_METHODS = TOOL_SPECS.filter((spec) => spec.execute === void 0 || spec.reachesDaemon === true).map((spec) => spec.engineMethod);
|
|
2293
2294
|
var TOOL_BY_NAME = new Map(
|
|
2294
2295
|
TOOL_SPECS.map((spec) => [spec.name, spec])
|
|
2295
2296
|
);
|
|
@@ -2689,6 +2690,10 @@ function renderTrailId(id, format) {
|
|
|
2689
2690
|
function renderTrailIds(ids, format) {
|
|
2690
2691
|
return ids.map((id) => renderTrailId(id, format)).join(", ");
|
|
2691
2692
|
}
|
|
2693
|
+
function renderErrorLine(text, format) {
|
|
2694
|
+
const stripped = text.replace(/[\u0000-\u001f\u007f]/g, "\xB7");
|
|
2695
|
+
return format === "html" ? escapeHTML(stripped) : stripped.replace(/[`[\]<>#*\\]/g, "\xB7");
|
|
2696
|
+
}
|
|
2692
2697
|
function describeFetchFailure(error) {
|
|
2693
2698
|
if (error instanceof EngineCallError) {
|
|
2694
2699
|
return formatToolErrorShape(error.toBody());
|
|
@@ -2698,7 +2703,11 @@ function describeFetchFailure(error) {
|
|
|
2698
2703
|
const [first] = mapped.content;
|
|
2699
2704
|
return first !== void 0 && first.type === "text" ? first.text : "the evidence read failed";
|
|
2700
2705
|
}
|
|
2701
|
-
return
|
|
2706
|
+
return formatToolError(
|
|
2707
|
+
GP_E_INTERNAL,
|
|
2708
|
+
String(error),
|
|
2709
|
+
"a fetch inside gp_recent_reports failed for a reason that is neither a daemon error frame nor an evidence-shape refusal, so this shell cannot name the cause from here. Read that operation on its own with gp_last_evidence {operationId} \u2014 it answers inside its own deadline and reports the fault by itself \u2014 or ask again with a smaller limit. Do not re-run gp_act to regenerate what the trail already holds"
|
|
2710
|
+
);
|
|
2702
2711
|
}
|
|
2703
2712
|
async function recentReports(args, context) {
|
|
2704
2713
|
const argv = args;
|
|
@@ -2781,7 +2790,11 @@ async function recentReports(args, context) {
|
|
|
2781
2790
|
}
|
|
2782
2791
|
if (packs.length === 0 && hardFailure !== null) {
|
|
2783
2792
|
const [first] = hardFailure.mapped.content;
|
|
2784
|
-
const base = first !== void 0 && first.type === "text" ? first.text :
|
|
2793
|
+
const base = first !== void 0 && first.type === "text" ? first.text : formatToolError(
|
|
2794
|
+
GP_E_INTERNAL,
|
|
2795
|
+
"the evidence read failed",
|
|
2796
|
+
"the fetch mapped to an answer this shell could not render as text, so no more than the failure itself is known here. Read the operation on its own with gp_last_evidence {operationId}, which reports its own fault with a remedy, or ask again with a smaller limit. Do not re-run gp_act to regenerate what the trail already holds"
|
|
2797
|
+
);
|
|
2785
2798
|
const extras = [];
|
|
2786
2799
|
if (notFetched.length > 0) {
|
|
2787
2800
|
extras.push(`${notFetched.length} further ${notFetched.length === 1 ? "entry" : "entries"} never fetched: ` + renderTrailIds(notFetched, format));
|
|
@@ -2824,7 +2837,7 @@ async function recentReports(args, context) {
|
|
|
2824
2837
|
const skipNote = skipped.length === 0 ? "" : note("gp-skipped", `skipped ${skipped.length} unreachable ${skipped.length === 1 ? "entry" : "entries"}: ${renderTrailIds(skipped, format)}`);
|
|
2825
2838
|
const unfetchedNote = notFetched.length === 0 ? "" : format === "html" ? `<p class="gp-not-fetched">not fetched within this report's ${RECENT_REPORTS_BUDGET_MS}ms wait: ${notFetched.length} ${notFetched.length === 1 ? "entry" : "entries"} left unanswered: ${renderTrailIds(notFetched, format)}. Fetch one by id with gp_last_evidence, or ask again with a smaller limit</p>` : `> not fetched within this report's ${RECENT_REPORTS_BUDGET_MS}ms wait: ${notFetched.length} ${notFetched.length === 1 ? "entry" : "entries"} left unanswered: ${renderTrailIds(notFetched, format)}. Fetch one by id with gp_last_evidence, or ask again with a smaller limit`;
|
|
2826
2839
|
const rejectNote = rejected.length === 0 ? "" : note("gp-rejected", `${rejectedClause(rejected.length)}${renderTrailIds(rejected, format)}. They were not sent, so nothing was lost by their absence here: gp_last_evidence validates an id the same way before it asks`);
|
|
2827
|
-
const failureNote = hardFailure === null ? "" : note("gp-fetch-failed", `the report stops here: ${renderTrailId(hardFailure.id, format)} failed with ${
|
|
2840
|
+
const failureNote = hardFailure === null ? "" : note("gp-fetch-failed", `the report stops here: ${renderTrailId(hardFailure.id, format)} failed with ${renderErrorLine(hardFailure.line, format)}. The ${packs.length} ${packs.length === 1 ? "pack" : "packs"} above were fetched and are rendered; read the failed one on its own with gp_last_evidence {operationId}. Do not re-run gp_act to regenerate what the trail already holds`);
|
|
2828
2841
|
const separator = format === "html" ? "\n<hr>\n" : "\n\n---\n";
|
|
2829
2842
|
const body = packs.map(({ pack }) => render2(pack, void 0)).join(separator);
|
|
2830
2843
|
const sections2 = [header];
|
package/dist/index.js
CHANGED
|
@@ -780,19 +780,28 @@ var DrainAwareWriter = class {
|
|
|
780
780
|
get failed() {
|
|
781
781
|
return this.dead !== null;
|
|
782
782
|
}
|
|
783
|
-
/**
|
|
784
|
-
*
|
|
783
|
+
/**
|
|
784
|
+
* How many replies this writer refused to queue. `gp_diagnose` reads it and
|
|
785
|
+
* surfaces it in-band (a reply dropped to a stalled stdout otherwise has no
|
|
786
|
+
* reader outside the logs): a silently-shrinking answer is the failure mode this
|
|
787
|
+
* ceiling replaces.
|
|
788
|
+
*/
|
|
785
789
|
get droppedFrames() {
|
|
786
790
|
return this.dropped;
|
|
787
791
|
}
|
|
788
|
-
/**
|
|
789
|
-
|
|
792
|
+
/**
|
|
793
|
+
* @param text already framed, including its trailing newline.
|
|
794
|
+
* @param requestId the JSON-RPC id the frame answers, so a drop names *which*
|
|
795
|
+
* reply the agent lost rather than only that one went missing.
|
|
796
|
+
*/
|
|
797
|
+
async write(text, requestId) {
|
|
790
798
|
if (this.failed) {
|
|
791
799
|
return false;
|
|
792
800
|
}
|
|
793
801
|
if (this.sink.writableLength >= this.backlogLimitBytes) {
|
|
794
802
|
this.dropped += 1;
|
|
795
|
-
|
|
803
|
+
const who = requestId === void 0 || requestId === null ? "a reply" : `reply ${JSON.stringify(requestId)}`;
|
|
804
|
+
this.report(`${who} 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
805
|
return false;
|
|
797
806
|
}
|
|
798
807
|
let flowing = true;
|
|
@@ -2143,7 +2152,7 @@ function daemonRestartNotice(filePath) {
|
|
|
2143
2152
|
return `Saved in ${filePath}, the projects file under the home record this user's state root defaults to. If this is the root the service was started on \u2014 which is not knowable from here \u2014 then the running background service loaded that file once when it started, so gp_attach will keep returning GP_E_NOT_FOUND for this projectId until the service restarts: ${restartCommand()}. ${rootDiscriminator()}`;
|
|
2144
2153
|
}
|
|
2145
2154
|
function rootDiscriminator() {
|
|
2146
|
-
return
|
|
2155
|
+
return 'Tell the two cases apart with the service\'s own answer: gp_project_list reads the file this call wrote, so an entry it lists is on disk, and gp_attach {projectId} is answered by the running service \u2014 an entry the list shows plus an attach that answers GP_E_NOT_FOUND means the service is loading a different projects.json than the one this write reached, not that the write failed. Which file the running service loads is not something this shell guesses from $HOME: `glasspaned --state-dir <root>` moves it to <root>/projects.json, and the daemon reports the root it resolved \u2014 `hello` names the state root under its `stateRoot` key. A daemon predating 1.15.0 omits the key, and an omitted key means "that daemon does not report it", never "so it must be the home default": for those binaries the discriminator below is the only read. Cross-check either way \u2014 `launchctl print gui/' + currentUid() + "/com.glasspane.daemon` prints the arguments the job was started with. If those arguments name a --state-dir, the entry has to go into that root's projects.json (repeat gp_project_set with " + PROJECTS_FILE_ENV + " pointed at <that root>/projects.json): no restart of the service will make it read anywhere else, so restarting again changes nothing. If they name no --state-dir, the service is on the default root and simply predates this write, and one restart with the command in this notice is what surfaces it.";
|
|
2147
2156
|
}
|
|
2148
2157
|
function restartCommand() {
|
|
2149
2158
|
return `run \`launchctl kickstart -k gui/${currentUid()}/com.glasspane.daemon\` \u2014 if that reports the service was not found, the launchd job was never bootstrapped, so ${daemonUnreachableRemedy()}`;
|
|
@@ -2467,7 +2476,7 @@ var TOOL_SPECS = [
|
|
|
2467
2476
|
additionalProperties: false,
|
|
2468
2477
|
properties: {
|
|
2469
2478
|
maxDepth: { type: "integer", minimum: 1, maximum: 10 },
|
|
2470
|
-
role: { type: "string", maxLength: SELECTOR_MAX_LENGTH }
|
|
2479
|
+
role: { type: "string", minLength: 1, maxLength: SELECTOR_MAX_LENGTH }
|
|
2471
2480
|
}
|
|
2472
2481
|
},
|
|
2473
2482
|
validate: zodBridge(ObserveArgs)
|
|
@@ -2533,7 +2542,8 @@ var TOOL_SPECS = [
|
|
|
2533
2542
|
}
|
|
2534
2543
|
},
|
|
2535
2544
|
validate: zodBridge(CaptureViewArgs),
|
|
2536
|
-
execute: captureView
|
|
2545
|
+
execute: captureView,
|
|
2546
|
+
reachesDaemon: true
|
|
2537
2547
|
},
|
|
2538
2548
|
{
|
|
2539
2549
|
name: "gp_diagnose",
|
|
@@ -2712,7 +2722,7 @@ var TOOL_SPECS = [
|
|
|
2712
2722
|
execute: projectGetTool
|
|
2713
2723
|
}
|
|
2714
2724
|
];
|
|
2715
|
-
var DAEMON_FORWARDED_METHODS = TOOL_SPECS.filter((spec) => spec.execute === void 0).map((spec) => spec.engineMethod);
|
|
2725
|
+
var DAEMON_FORWARDED_METHODS = TOOL_SPECS.filter((spec) => spec.execute === void 0 || spec.reachesDaemon === true).map((spec) => spec.engineMethod);
|
|
2716
2726
|
var TOOL_BY_NAME = new Map(
|
|
2717
2727
|
TOOL_SPECS.map((spec) => [spec.name, spec])
|
|
2718
2728
|
);
|
|
@@ -2895,7 +2905,7 @@ function shellFaultRemedy(engineMethod) {
|
|
|
2895
2905
|
}
|
|
2896
2906
|
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}`;
|
|
2897
2907
|
}
|
|
2898
|
-
async function executeTool(spec, args, engine, session) {
|
|
2908
|
+
async function executeTool(spec, args, engine, session, stdoutDroppedFrames) {
|
|
2899
2909
|
if (!session) {
|
|
2900
2910
|
return {
|
|
2901
2911
|
content: [{ type: "text", text: formatToolError(
|
|
@@ -2919,16 +2929,16 @@ async function executeTool(spec, args, engine, session) {
|
|
|
2919
2929
|
}
|
|
2920
2930
|
const advised = engineWithPayloadAdvice(engine, spec, checked.value);
|
|
2921
2931
|
if (!trailScopedTool(spec)) {
|
|
2922
|
-
return runValidatedTool(spec, checked.value, advised, session, INERT_TRAIL_TURN);
|
|
2932
|
+
return runValidatedTool(spec, checked.value, advised, session, INERT_TRAIL_TURN, stdoutDroppedFrames);
|
|
2923
2933
|
}
|
|
2924
2934
|
const turn = session.claimTrailTurn();
|
|
2925
2935
|
try {
|
|
2926
|
-
return await runValidatedTool(spec, checked.value, advised, session, turn);
|
|
2936
|
+
return await runValidatedTool(spec, checked.value, advised, session, turn, stdoutDroppedFrames);
|
|
2927
2937
|
} finally {
|
|
2928
2938
|
turn.release();
|
|
2929
2939
|
}
|
|
2930
2940
|
}
|
|
2931
|
-
async function runValidatedTool(spec, value, engine, session, turn) {
|
|
2941
|
+
async function runValidatedTool(spec, value, engine, session, turn, stdoutDroppedFrames) {
|
|
2932
2942
|
if (spec.execute !== void 0) {
|
|
2933
2943
|
return spec.execute(value, { engine, session, turn });
|
|
2934
2944
|
}
|
|
@@ -2955,6 +2965,15 @@ async function runValidatedTool(spec, value, engine, session, turn) {
|
|
|
2955
2965
|
${caBundleDiagnosis(reading.ca, updaterEnableCommand(reading.runtime))}
|
|
2956
2966
|
${runtimeSelfDiagnosis(reading.runtime, reading.stateFile)}` : reading.summary
|
|
2957
2967
|
});
|
|
2968
|
+
if (stdoutDroppedFrames !== void 0) {
|
|
2969
|
+
const dropped = stdoutDroppedFrames();
|
|
2970
|
+
if (dropped > 0) {
|
|
2971
|
+
content.push({
|
|
2972
|
+
type: "text",
|
|
2973
|
+
text: `stdout backlog: this shell dropped ${dropped} ${dropped === 1 ? "reply" : "replies"} since startup because the reader on the other end stopped consuming them, so an answer you expected to receive may never have been written. The operation itself is unaffected \u2014 this is the byte stream, not the daemon. Drain stdout (close whatever is holding the pipe), then re-read the specific operation with gp_last_evidence {operationId}; do not re-run gp_act to regenerate work the trail already records`
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2958
2977
|
}
|
|
2959
2978
|
return {
|
|
2960
2979
|
content,
|
|
@@ -3357,6 +3376,10 @@ function renderTrailId(id, format) {
|
|
|
3357
3376
|
function renderTrailIds(ids, format) {
|
|
3358
3377
|
return ids.map((id) => renderTrailId(id, format)).join(", ");
|
|
3359
3378
|
}
|
|
3379
|
+
function renderErrorLine(text, format) {
|
|
3380
|
+
const stripped = text.replace(/[\u0000-\u001f\u007f]/g, "\xB7");
|
|
3381
|
+
return format === "html" ? escapeHTML(stripped) : stripped.replace(/[`[\]<>#*\\]/g, "\xB7");
|
|
3382
|
+
}
|
|
3360
3383
|
function describeFetchFailure(error) {
|
|
3361
3384
|
if (error instanceof EngineCallError) {
|
|
3362
3385
|
return formatToolErrorShape(error.toBody());
|
|
@@ -3366,7 +3389,11 @@ function describeFetchFailure(error) {
|
|
|
3366
3389
|
const [first] = mapped.content;
|
|
3367
3390
|
return first !== void 0 && first.type === "text" ? first.text : "the evidence read failed";
|
|
3368
3391
|
}
|
|
3369
|
-
return
|
|
3392
|
+
return formatToolError(
|
|
3393
|
+
GP_E_INTERNAL,
|
|
3394
|
+
String(error),
|
|
3395
|
+
"a fetch inside gp_recent_reports failed for a reason that is neither a daemon error frame nor an evidence-shape refusal, so this shell cannot name the cause from here. Read that operation on its own with gp_last_evidence {operationId} \u2014 it answers inside its own deadline and reports the fault by itself \u2014 or ask again with a smaller limit. Do not re-run gp_act to regenerate what the trail already holds"
|
|
3396
|
+
);
|
|
3370
3397
|
}
|
|
3371
3398
|
async function recentReports(args, context) {
|
|
3372
3399
|
const argv = args;
|
|
@@ -3449,7 +3476,11 @@ async function recentReports(args, context) {
|
|
|
3449
3476
|
}
|
|
3450
3477
|
if (packs.length === 0 && hardFailure !== null) {
|
|
3451
3478
|
const [first] = hardFailure.mapped.content;
|
|
3452
|
-
const base = first !== void 0 && first.type === "text" ? first.text :
|
|
3479
|
+
const base = first !== void 0 && first.type === "text" ? first.text : formatToolError(
|
|
3480
|
+
GP_E_INTERNAL,
|
|
3481
|
+
"the evidence read failed",
|
|
3482
|
+
"the fetch mapped to an answer this shell could not render as text, so no more than the failure itself is known here. Read the operation on its own with gp_last_evidence {operationId}, which reports its own fault with a remedy, or ask again with a smaller limit. Do not re-run gp_act to regenerate what the trail already holds"
|
|
3483
|
+
);
|
|
3453
3484
|
const extras = [];
|
|
3454
3485
|
if (notFetched.length > 0) {
|
|
3455
3486
|
extras.push(`${notFetched.length} further ${notFetched.length === 1 ? "entry" : "entries"} never fetched: ` + renderTrailIds(notFetched, format));
|
|
@@ -3492,7 +3523,7 @@ async function recentReports(args, context) {
|
|
|
3492
3523
|
const skipNote = skipped.length === 0 ? "" : note("gp-skipped", `skipped ${skipped.length} unreachable ${skipped.length === 1 ? "entry" : "entries"}: ${renderTrailIds(skipped, format)}`);
|
|
3493
3524
|
const unfetchedNote = notFetched.length === 0 ? "" : format === "html" ? `<p class="gp-not-fetched">not fetched within this report's ${RECENT_REPORTS_BUDGET_MS}ms wait: ${notFetched.length} ${notFetched.length === 1 ? "entry" : "entries"} left unanswered: ${renderTrailIds(notFetched, format)}. Fetch one by id with gp_last_evidence, or ask again with a smaller limit</p>` : `> not fetched within this report's ${RECENT_REPORTS_BUDGET_MS}ms wait: ${notFetched.length} ${notFetched.length === 1 ? "entry" : "entries"} left unanswered: ${renderTrailIds(notFetched, format)}. Fetch one by id with gp_last_evidence, or ask again with a smaller limit`;
|
|
3494
3525
|
const rejectNote = rejected.length === 0 ? "" : note("gp-rejected", `${rejectedClause(rejected.length)}${renderTrailIds(rejected, format)}. They were not sent, so nothing was lost by their absence here: gp_last_evidence validates an id the same way before it asks`);
|
|
3495
|
-
const failureNote = hardFailure === null ? "" : note("gp-fetch-failed", `the report stops here: ${renderTrailId(hardFailure.id, format)} failed with ${
|
|
3526
|
+
const failureNote = hardFailure === null ? "" : note("gp-fetch-failed", `the report stops here: ${renderTrailId(hardFailure.id, format)} failed with ${renderErrorLine(hardFailure.line, format)}. The ${packs.length} ${packs.length === 1 ? "pack" : "packs"} above were fetched and are rendered; read the failed one on its own with gp_last_evidence {operationId}. Do not re-run gp_act to regenerate what the trail already holds`);
|
|
3496
3527
|
const separator = format === "html" ? "\n<hr>\n" : "\n\n---\n";
|
|
3497
3528
|
const body = packs.map(({ pack }) => render(pack, void 0)).join(separator);
|
|
3498
3529
|
const sections2 = [header];
|
|
@@ -3793,7 +3824,7 @@ var SUPPORTED_PROTOCOL_VERSIONS = [
|
|
|
3793
3824
|
"2025-03-26",
|
|
3794
3825
|
MCP_PROTOCOL_VERSION
|
|
3795
3826
|
];
|
|
3796
|
-
var SERVER_INFO = { name: "glasspane-mcp", version: "1.
|
|
3827
|
+
var SERVER_INFO = { name: "glasspane-mcp", version: "1.15.0" };
|
|
3797
3828
|
var PARSE_ERROR = -32700;
|
|
3798
3829
|
var INVALID_REQUEST = -32600;
|
|
3799
3830
|
var METHOD_NOT_FOUND = -32601;
|
|
@@ -3912,7 +3943,13 @@ var McpServer = class {
|
|
|
3912
3943
|
if (!spec) {
|
|
3913
3944
|
return this.error(INVALID_PARAMS, `Unknown tool: ${name}`, id);
|
|
3914
3945
|
}
|
|
3915
|
-
const outcome = await executeTool(
|
|
3946
|
+
const outcome = await executeTool(
|
|
3947
|
+
spec,
|
|
3948
|
+
params.arguments ?? {},
|
|
3949
|
+
this.deps.engine,
|
|
3950
|
+
this.session,
|
|
3951
|
+
this.deps.stdoutDroppedFrames
|
|
3952
|
+
);
|
|
3916
3953
|
return this.result(id, { content: outcome.content, isError: outcome.isError });
|
|
3917
3954
|
}
|
|
3918
3955
|
asParams(rawParams) {
|
|
@@ -3935,7 +3972,7 @@ var McpServer = class {
|
|
|
3935
3972
|
return { jsonrpc: JSONRPC, id, error: { code, message } };
|
|
3936
3973
|
}
|
|
3937
3974
|
};
|
|
3938
|
-
function createTrackedMcpServer(engine, report) {
|
|
3975
|
+
function createTrackedMcpServer(engine, report, stdoutDroppedFrames) {
|
|
3939
3976
|
const session = new EvidenceAuditSession();
|
|
3940
3977
|
engine.onLateReply(({ method, result, correlation, attribution }) => {
|
|
3941
3978
|
if (correlation !== void 0 && correlation !== session.generation) {
|
|
@@ -3969,7 +4006,7 @@ function createTrackedMcpServer(engine, report) {
|
|
|
3969
4006
|
}
|
|
3970
4007
|
);
|
|
3971
4008
|
});
|
|
3972
|
-
return { server: new McpServer({ engine, session, report }), session };
|
|
4009
|
+
return { server: new McpServer({ engine, session, report, stdoutDroppedFrames }), session };
|
|
3973
4010
|
}
|
|
3974
4011
|
function describeAttach(result) {
|
|
3975
4012
|
if (typeof result !== "object" || result === null) {
|
|
@@ -4113,12 +4150,13 @@ function main() {
|
|
|
4113
4150
|
const replies = new DrainAwareWriter(process2.stdout, logNote, DRAIN_WAIT_MS, () => shutdown());
|
|
4114
4151
|
const queue = new OrderedReplyQueue((error) => logNote(`reply failed: ${String(error)}`));
|
|
4115
4152
|
engine.onEngineNote(logNote);
|
|
4116
|
-
const server = createTrackedMcpServer(engine, logNote).server;
|
|
4153
|
+
const server = createTrackedMcpServer(engine, logNote, () => replies.droppedFrames).server;
|
|
4117
4154
|
const writeResponse = async (response) => {
|
|
4118
4155
|
if (response === null) {
|
|
4119
4156
|
return;
|
|
4120
4157
|
}
|
|
4121
|
-
|
|
4158
|
+
const id = response.id;
|
|
4159
|
+
await replies.write(canonicalJson(response) + "\n", id);
|
|
4122
4160
|
};
|
|
4123
4161
|
const reader = new LineReader({
|
|
4124
4162
|
onLine: (line) => {
|
|
@@ -4138,7 +4176,7 @@ function main() {
|
|
|
4138
4176
|
queue.push(async () => shutdown());
|
|
4139
4177
|
});
|
|
4140
4178
|
process2.stdin.on("close", () => queue.push(async () => shutdown()));
|
|
4141
|
-
process2.on("SIGINT", shutdown);
|
|
4142
|
-
process2.on("SIGTERM", shutdown);
|
|
4179
|
+
process2.on("SIGINT", () => queue.push(async () => shutdown()));
|
|
4180
|
+
process2.on("SIGTERM", () => queue.push(async () => shutdown()));
|
|
4143
4181
|
}
|
|
4144
4182
|
main();
|
package/dist/io.d.ts
CHANGED
|
@@ -155,11 +155,19 @@ export declare class DrainAwareWriter {
|
|
|
155
155
|
private dropped;
|
|
156
156
|
constructor(sink: Writable, report: (note: string) => void, drainWaitMs: number, onDead: (error: Error) => void, backlogLimitBytes?: number);
|
|
157
157
|
get failed(): boolean;
|
|
158
|
-
/**
|
|
159
|
-
*
|
|
158
|
+
/**
|
|
159
|
+
* How many replies this writer refused to queue. `gp_diagnose` reads it and
|
|
160
|
+
* surfaces it in-band (a reply dropped to a stalled stdout otherwise has no
|
|
161
|
+
* reader outside the logs): a silently-shrinking answer is the failure mode this
|
|
162
|
+
* ceiling replaces.
|
|
163
|
+
*/
|
|
160
164
|
get droppedFrames(): number;
|
|
161
|
-
/**
|
|
162
|
-
|
|
165
|
+
/**
|
|
166
|
+
* @param text already framed, including its trailing newline.
|
|
167
|
+
* @param requestId the JSON-RPC id the frame answers, so a drop names *which*
|
|
168
|
+
* reply the agent lost rather than only that one went missing.
|
|
169
|
+
*/
|
|
170
|
+
write(text: string, requestId?: number | string | null): Promise<boolean>;
|
|
163
171
|
private waitForDrain;
|
|
164
172
|
}
|
|
165
173
|
//# sourceMappingURL=io.d.ts.map
|
|
@@ -147,7 +147,8 @@ export declare function projectSet(args: ProjectSetArgs): ProjectSetResult;
|
|
|
147
147
|
* with no state root. `glasspaned --state-dir <root>` makes it load
|
|
148
148
|
* `<root>/projects.json` (`ProjectRegistry(stateRoot:)`, and every subcommand
|
|
149
149
|
* resolves through the same root), and the socket cannot be asked which root the
|
|
150
|
-
* running job chose —
|
|
150
|
+
* running job chose — since 1.15.0 the daemon says so in `hello`'s `stateRoot`, while a
|
|
151
|
+
* service still running an older binary does not. So a
|
|
151
152
|
* notice that simply ordered a restart was telling an agent that restarting would
|
|
152
153
|
* help in the one case where it cannot, and a notice that said "this is the file
|
|
153
154
|
* the service loads" was naming a file the service may not load. Both branches
|
package/dist/tools.d.ts
CHANGED
|
@@ -258,6 +258,15 @@ export interface ToolSpec {
|
|
|
258
258
|
* packs and render them through the report generator.
|
|
259
259
|
*/
|
|
260
260
|
execute?: (args: Record<string, unknown>, context: ToolExecuteContext) => Promise<ToolResult>;
|
|
261
|
+
/**
|
|
262
|
+
* True for a tool that has its own `execute` yet still puts a real daemon
|
|
263
|
+
* method on the wire (`gp_capture_view`'s executor calls `capture_view`), so
|
|
264
|
+
* `hello.capabilities` reconciliation must count its `engineMethod` as
|
|
265
|
+
* forwarded. Left unset on genuinely shell-only tools (`gp_recent_reports`,
|
|
266
|
+
* the project tools), whose `engineMethod` is a pseudo-method the daemon does
|
|
267
|
+
* not advertise and would answer with `methodNotFound`.
|
|
268
|
+
*/
|
|
269
|
+
reachesDaemon?: true;
|
|
261
270
|
}
|
|
262
271
|
export interface ToolExecuteContext {
|
|
263
272
|
engine: EngineJsonRpcClient;
|
|
@@ -294,6 +303,15 @@ export declare const TOOL_SPECS: readonly ToolSpec[];
|
|
|
294
303
|
* (`engine-client.describeCapabilityDrift`): a hand-copied second truth is the
|
|
295
304
|
* exact drift the reader exists to catch, and the gateway already derives its own
|
|
296
305
|
* forwardable set from `execute === undefined` for the same reason.
|
|
306
|
+
*
|
|
307
|
+
* `execute === undefined` alone under-counts: `gp_capture_view` has its own
|
|
308
|
+
* executor, yet that executor calls `capture_view` on the wire, so the daemon
|
|
309
|
+
* advertises a method the agent *can* reach and the reconciliation would report
|
|
310
|
+
* as unreachable from MCP. `reachesDaemon` marks exactly those executor tools
|
|
311
|
+
* that still forward a real daemon method; `test/shell-2026-defects.test.mjs`
|
|
312
|
+
* cross-checks this set against both the daemon's advertised capability names and
|
|
313
|
+
* the literal `.call("…")` sites so neither a new omission nor a stale pseudo-
|
|
314
|
+
* method survives silently.
|
|
297
315
|
*/
|
|
298
316
|
export declare const DAEMON_FORWARDED_METHODS: readonly string[];
|
|
299
317
|
export declare const TOOL_BY_NAME: ReadonlyMap<string, ToolSpec>;
|
|
@@ -422,7 +440,14 @@ export declare function executeTool(spec: ToolSpec, args: unknown, engine: Engin
|
|
|
422
440
|
* meaningful caller here is JavaScript (`dist` consumers, the smoke scripts, the
|
|
423
441
|
* tests), where an omitted argument compiles forever and fails silently.
|
|
424
442
|
*/
|
|
425
|
-
session: EvidenceAuditSession
|
|
443
|
+
session: EvidenceAuditSession,
|
|
444
|
+
/**
|
|
445
|
+
* Reads how many replies this shell's stdout writer dropped under backpressure,
|
|
446
|
+
* so `gp_diagnose` can surface a lost reply in-band. Optional: only `index.ts`
|
|
447
|
+
* owns the writer, and a caller without one (tests, the HTTP gateway) leaves it
|
|
448
|
+
* undefined and the tool stays silent.
|
|
449
|
+
*/
|
|
450
|
+
stdoutDroppedFrames?: () => number): Promise<ToolResult>;
|
|
426
451
|
/**
|
|
427
452
|
* Strongly validates the engine's last_evidence result (§6.3) and returns the
|
|
428
453
|
* parsed pack together with the schema label the archive *carried*. The engine
|
|
@@ -451,6 +476,14 @@ export declare function parseEvidenceFrame(raw: unknown): {
|
|
|
451
476
|
* byte-identical to the shared golden.
|
|
452
477
|
*/
|
|
453
478
|
export declare function packForReport(pack: EvidencePack, measuredSchemaVersion: string): EvidencePackReportView;
|
|
479
|
+
/**
|
|
480
|
+
* One fetch failure, in the one line a partial report has room for.
|
|
481
|
+
* Exported for `test/shell-2026-defects.test.mjs`, which pins the
|
|
482
|
+
* `{code, message, remedy}` shape of the fallback arm: that arm is what a
|
|
483
|
+
* non-daemon, non-shape error reaches, and a two-field string there is an error
|
|
484
|
+
* frame leaving the shell with no next move.
|
|
485
|
+
*/
|
|
486
|
+
export declare function describeFetchFailure(error: unknown): string;
|
|
454
487
|
export type { Selector, Action, AssertionProperty };
|
|
455
488
|
/**
|
|
456
489
|
* The engine view handed to one tool: the same client, with one kind of error's
|
package/package.json
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://schemas.iterate.dev/convergence-round-state-0.1.json",
|
|
4
|
+
"title": "ConvergenceRoundState",
|
|
5
|
+
"description": "What a caller states about the round that just finished, which the convergence rules turn into an action (5.7 R40 scenario 4). Every field is a verdict somebody else produced; this contract moves none of them. NOTE ON SOURCE OF TRUTH: the field constraints are duplicated from `src/convergence.ts`, which is the definition; this file mirrors it. The ordering that decides which verdict wins (breaker, then budget, then errored check, then findings, then evidence, then invariants, then converged) is arithmetic in the validator and cannot be expressed in JSON Schema — a schema-only implementation must re-implement `convergenceDecision` and be tested against the cases in `fixtures/convergence.ok-01.json`.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["round", "maxRounds", "invariantState", "openFindings", "evidenceComplete", "breakerLevel"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": {
|
|
11
|
+
"const": "iterate.convergence/0.1"
|
|
12
|
+
},
|
|
13
|
+
"round": {
|
|
14
|
+
"type": "integer",
|
|
15
|
+
"minimum": 1
|
|
16
|
+
},
|
|
17
|
+
"maxRounds": {
|
|
18
|
+
"type": "integer",
|
|
19
|
+
"minimum": 1
|
|
20
|
+
},
|
|
21
|
+
"invariantState": {
|
|
22
|
+
"enum": ["pass", "fail", "error", "unmeasured"]
|
|
23
|
+
},
|
|
24
|
+
"openFindings": {
|
|
25
|
+
"type": "integer",
|
|
26
|
+
"minimum": 0
|
|
27
|
+
},
|
|
28
|
+
"evidenceComplete": {
|
|
29
|
+
"type": "boolean"
|
|
30
|
+
},
|
|
31
|
+
"breakerLevel": {
|
|
32
|
+
"type": "integer",
|
|
33
|
+
"minimum": 0,
|
|
34
|
+
"maximum": 3
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -40,6 +40,26 @@
|
|
|
40
40
|
"createdAt": {
|
|
41
41
|
"type": "string",
|
|
42
42
|
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$"
|
|
43
|
+
},
|
|
44
|
+
"kind": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"pattern": "^[a-z][a-z0-9_]{0,31}$",
|
|
47
|
+
"description": "What sort of decision this line records, in the recording ecosystem's own vocabulary (iterate uses round_start / review_result / atomic_fix / …, the GlassPane fork records tool executions). Deliberately NOT an enum: the kernel does not own either vocabulary, and enumerating them would make every new producer kind a kernel release. See the module doc in src/decision-log-entry.ts."
|
|
48
|
+
},
|
|
49
|
+
"round": {
|
|
50
|
+
"type": "integer",
|
|
51
|
+
"minimum": 0,
|
|
52
|
+
"description": "Loop position the decision belongs to, when the producer counts rounds. Optional because a one-off tool execution is not part of a round."
|
|
53
|
+
},
|
|
54
|
+
"dimension": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
57
|
+
"maxLength": 64,
|
|
58
|
+
"description": "The dimension this decision was serving, using the same id rule as dimension-context (DIMENSION_ID_PATTERN / DIMENSION_ID_MAX_LENGTH, imported not restated). This is what lets a recorded decision count toward 'N/M dimensions verified' from either ecosystem."
|
|
59
|
+
},
|
|
60
|
+
"payload": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"description": "Structured detail carried alongside the human-readable summary — the field the legacy 'data' object maps onto. NOTE A BOUND THE SCHEMA CANNOT STATE: the canonical-JSON form of this object is limited to PAYLOAD_MAX_CANONICAL_LENGTH (8192) characters by the TypeScript validator and the Python port, because an unbounded field in a hash-chained ledger lets a line's bytes and digest grow without limit. JSON Schema has no way to bound serialized length, so this is a documented divergence, pinned as a named case in kernel/test/decision-log-entry.test.mjs rather than silently allowed."
|
|
43
63
|
}
|
|
44
64
|
}
|
|
45
65
|
}
|
|
@@ -197,16 +197,19 @@
|
|
|
197
197
|
"role": {
|
|
198
198
|
"type": "string",
|
|
199
199
|
"minLength": 1,
|
|
200
|
-
"maxLength": 512
|
|
200
|
+
"maxLength": 512,
|
|
201
|
+
"description": "What the target is, in the accessibility tree's own vocabulary. Never empty: the engine's matcher reads an empty role as 'no constraint stated', so an empty role stops naming what the assertion targets instead of narrowing it — and the daemon refuses it."
|
|
201
202
|
},
|
|
202
203
|
"title": {
|
|
203
204
|
"type": "string",
|
|
204
205
|
"minLength": 1,
|
|
205
|
-
"maxLength": 512
|
|
206
|
+
"maxLength": 512,
|
|
207
|
+
"description": "Optional display label. When present it is never empty, for the same reason as role: a report rendered from `title: \"\"` prints a missing label and reads as 'the tree had no title there'."
|
|
206
208
|
},
|
|
207
209
|
"identifier": {
|
|
208
210
|
"type": "string",
|
|
209
|
-
"maxLength": 512
|
|
211
|
+
"maxLength": 512,
|
|
212
|
+
"description": "Optional AXIdentifier. Deliberately has no minLength: no side rules on an empty identifier (the daemon has no guard for it), so the kernel does not invent one."
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
},
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://schemas.iterate.dev/invariant-report-0.1.json",
|
|
4
|
+
"title": "InvariantReport",
|
|
5
|
+
"description": "What the invariant runner observed when it checked a recipe's invariants (5.7 R40 scenario 1: the failure that has to reach the other harness). Whoever runs the checks produces this; whoever decides whether to re-verify reads it. A check kind of `refused` means the runner declined to execute a configured command because it carries shell metacharacters or is empty — it is a violation, so a report whose only failing check was refused is `fail`, never `error`. NOTE ON SOURCE OF TRUTH: the check-name and detail constraints are duplicated from `src/invariant-report.ts`, which is the definition; this file mirrors it. The outcome-vs-checks rule IS expressible here and is expressed twice on purpose: the `allOf` branches below and the zod `superRefine` must refuse the same reports, so a second implementation that only validates JSON Schema still cannot carry a pass over a failed check.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "outcome", "checks"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": {
|
|
11
|
+
"const": "iterate.invariant-report/0.1"
|
|
12
|
+
},
|
|
13
|
+
"outcome": {
|
|
14
|
+
"enum": ["pass", "fail", "error"]
|
|
15
|
+
},
|
|
16
|
+
"checks": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"minItems": 1,
|
|
19
|
+
"maxItems": 256,
|
|
20
|
+
"items": {
|
|
21
|
+
"$ref": "#/$defs/invariantCheck"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"round": {
|
|
25
|
+
"type": "integer",
|
|
26
|
+
"minimum": 1
|
|
27
|
+
},
|
|
28
|
+
"durationMs": {
|
|
29
|
+
"type": "integer",
|
|
30
|
+
"minimum": 0
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"allOf": [
|
|
34
|
+
{
|
|
35
|
+
"if": {
|
|
36
|
+
"properties": {
|
|
37
|
+
"outcome": {
|
|
38
|
+
"const": "pass"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"required": ["outcome"]
|
|
42
|
+
},
|
|
43
|
+
"then": {
|
|
44
|
+
"properties": {
|
|
45
|
+
"checks": {
|
|
46
|
+
"items": {
|
|
47
|
+
"properties": {
|
|
48
|
+
"ok": {
|
|
49
|
+
"const": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"required": ["ok"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"if": {
|
|
60
|
+
"properties": {
|
|
61
|
+
"outcome": {
|
|
62
|
+
"const": "fail"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"required": ["outcome"]
|
|
66
|
+
},
|
|
67
|
+
"then": {
|
|
68
|
+
"properties": {
|
|
69
|
+
"checks": {
|
|
70
|
+
"contains": {
|
|
71
|
+
"properties": {
|
|
72
|
+
"ok": {
|
|
73
|
+
"const": false
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"required": ["ok"]
|
|
77
|
+
},
|
|
78
|
+
"minContains": 1
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
"$defs": {
|
|
85
|
+
"invariantCheck": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"additionalProperties": false,
|
|
88
|
+
"required": ["name", "kind", "ok"],
|
|
89
|
+
"properties": {
|
|
90
|
+
"name": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"minLength": 1,
|
|
93
|
+
"maxLength": 256
|
|
94
|
+
},
|
|
95
|
+
"kind": {
|
|
96
|
+
"enum": ["ensure", "command", "refused"]
|
|
97
|
+
},
|
|
98
|
+
"ok": {
|
|
99
|
+
"type": "boolean"
|
|
100
|
+
},
|
|
101
|
+
"detail": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"maxLength": 4096
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://schemas.iterate.dev/provider-registration-0.1.json",
|
|
4
|
+
"title": "ProviderRegistration",
|
|
5
|
+
"description": "Metadata for one judgement producer, so both harnesses can consume the same provider through the same shape (5.7 R40 scenario 5). It declares who provides what and what they need; it never carries a judgement, which stays in the engine and the reviewers. NOTE ON SOURCE OF TRUTH: the field constraints are duplicated from `src/provider-registration.ts`, which is the definition; this file mirrors it. METHOD-LEVEL UNIQUENESS IS ENFORCED BY ZOD ONLY: `uniqueItems` compares whole objects, so two methods with the same name but different `requires` pass this schema and are refused by `ProviderRegistrationSchema`. The registry semantics (refusing to overwrite an id, no default provider, no fuzzy resolve) are functions in the validator, not schema features.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "id", "kind", "version", "methods"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": {
|
|
11
|
+
"const": "iterate.provider/0.1"
|
|
12
|
+
},
|
|
13
|
+
"id": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
16
|
+
"maxLength": 64
|
|
17
|
+
},
|
|
18
|
+
"kind": {
|
|
19
|
+
"enum": ["ae-diagnosis", "report-render", "evidence-source"]
|
|
20
|
+
},
|
|
21
|
+
"version": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"minLength": 1,
|
|
24
|
+
"maxLength": 64
|
|
25
|
+
},
|
|
26
|
+
"origin": {
|
|
27
|
+
"enum": ["kernel", "glasspane", "iterate", "plugin"]
|
|
28
|
+
},
|
|
29
|
+
"methods": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"minItems": 1,
|
|
32
|
+
"maxItems": 32,
|
|
33
|
+
"uniqueItems": true,
|
|
34
|
+
"items": {
|
|
35
|
+
"$ref": "#/$defs/providerMethod"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"$defs": {
|
|
40
|
+
"providerMethod": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"required": ["name", "requires"],
|
|
44
|
+
"properties": {
|
|
45
|
+
"name": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
48
|
+
"maxLength": 64
|
|
49
|
+
},
|
|
50
|
+
"requires": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"maxItems": 16,
|
|
53
|
+
"uniqueItems": true,
|
|
54
|
+
"items": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
57
|
+
"maxLength": 64
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://schemas.iterate.dev/reverification-request-0.1.json",
|
|
4
|
+
"title": "ReverificationRequest",
|
|
5
|
+
"description": "The ask that comes out of a failed invariant report (5.7 R40 scenario 1): re-run these recipe steps, dimensions, or operations. It cites the failure by digest rather than embedding it, so the ledger line that records the re-verification can be checked against the ledger line that records the failure. NOTE ON SOURCE OF TRUTH: the field constraints are duplicated from `src/invariant-report.ts`, which is the definition; this file mirrors it. The 'an empty target set is not a re-verification' rule IS expressible here and is expressed twice on purpose, via `anyOf`, so a schema-only implementation refuses it too.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "trigger", "targets"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": {
|
|
11
|
+
"const": "iterate.reverification/0.1"
|
|
12
|
+
},
|
|
13
|
+
"trigger": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"additionalProperties": false,
|
|
16
|
+
"required": ["kind", "reportDigest"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"kind": {
|
|
19
|
+
"const": "invariant"
|
|
20
|
+
},
|
|
21
|
+
"reportDigest": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"pattern": "^iv_[0-9a-f]{64}$"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"targets": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"additionalProperties": false,
|
|
30
|
+
"anyOf": [
|
|
31
|
+
{
|
|
32
|
+
"required": ["recipeSteps"]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"required": ["dimensions"]
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"required": ["operationIds"]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"properties": {
|
|
42
|
+
"recipeSteps": {
|
|
43
|
+
"type": "array",
|
|
44
|
+
"minItems": 1,
|
|
45
|
+
"maxItems": 64,
|
|
46
|
+
"items": {
|
|
47
|
+
"$ref": "#/$defs/recipeStep"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"dimensions": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"minItems": 1,
|
|
53
|
+
"maxItems": 64,
|
|
54
|
+
"uniqueItems": true,
|
|
55
|
+
"items": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
58
|
+
"maxLength": 64
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"operationIds": {
|
|
62
|
+
"type": "array",
|
|
63
|
+
"minItems": 1,
|
|
64
|
+
"maxItems": 64,
|
|
65
|
+
"uniqueItems": true,
|
|
66
|
+
"items": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"pattern": "^op_[0-9A-HJKMNP-TV-Z]{26}$"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"$defs": {
|
|
75
|
+
"recipeStep": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"additionalProperties": false,
|
|
78
|
+
"required": ["kind", "params"],
|
|
79
|
+
"properties": {
|
|
80
|
+
"kind": {
|
|
81
|
+
"enum": ["act", "observe", "assert", "diagnose"]
|
|
82
|
+
},
|
|
83
|
+
"params": {
|
|
84
|
+
"type": "object"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://schemas.iterate.dev/run-plan-0.1.json",
|
|
4
|
+
"title": "RunPlan",
|
|
5
|
+
"description": "Which review dimensions a run intends to cover, in planned order (5.7 R40 scenario 2: dimension-aware context compression needs a plan to be aware of). Whoever configures the run produces this; whoever renders coverage reads it. Nothing in the kernel reads a config file to make one. NOTE ON SOURCE OF TRUTH: the id/label/focus constraints are duplicated from `src/dimension-context.ts`, which has no JSON Schema of its own — until that gap is closed, the zod side is the definition and this file mirrors it. ID-LEVEL UNIQUENESS IS ENFORCED BY ZOD ONLY: `uniqueItems` compares whole objects, so two entries with the same id but different focus pass this schema and are refused by `RunPlanSchema`.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "dimensions"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": {
|
|
11
|
+
"const": "iterate.run-plan/0.1"
|
|
12
|
+
},
|
|
13
|
+
"dimensions": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"minItems": 1,
|
|
16
|
+
"maxItems": 64,
|
|
17
|
+
"uniqueItems": true,
|
|
18
|
+
"items": {
|
|
19
|
+
"$ref": "#/$defs/plannedDimension"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"source": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"required": ["kind"],
|
|
26
|
+
"properties": {
|
|
27
|
+
"kind": {
|
|
28
|
+
"enum": ["config", "recipe", "session", "manual"]
|
|
29
|
+
},
|
|
30
|
+
"name": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"minLength": 1,
|
|
33
|
+
"maxLength": 512
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"$defs": {
|
|
39
|
+
"plannedDimension": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"required": ["id"],
|
|
43
|
+
"properties": {
|
|
44
|
+
"id": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
47
|
+
"maxLength": 64
|
|
48
|
+
},
|
|
49
|
+
"label": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"maxLength": 256
|
|
52
|
+
},
|
|
53
|
+
"focus": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"maxLength": 4096
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|