glasspane-mcp 1.18.0 → 1.19.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 +13 -0
- package/dist/errors.d.ts +1 -0
- package/dist/http-gateway-cli.js +294 -40
- package/dist/http-gateway.d.ts +7 -0
- package/dist/http-gateway.js +294 -40
- package/dist/index.js +350 -96
- package/dist/providers.d.ts +226 -0
- package/dist/stdio-session.js +350 -96
- package/dist/update-state.d.ts +4 -5
- 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.19.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
|
@@ -439,6 +439,19 @@ export declare class EngineJsonRpcClient {
|
|
|
439
439
|
*/
|
|
440
440
|
private helloStateRoot;
|
|
441
441
|
get reportedStateRoot(): string | null;
|
|
442
|
+
/**
|
|
443
|
+
* The version the daemon named in its own `hello`, or null when no handshake on this
|
|
444
|
+
* connection has reported one.
|
|
445
|
+
*
|
|
446
|
+
* This is the *producer's* measurement, and it is what the provider attribution line
|
|
447
|
+
* states (`providers.ts`'s `attributeCapability`): the verdict an agent is being told
|
|
448
|
+
* about came out of the daemon's build, not this package's. Read rather than assumed —
|
|
449
|
+
* `null` means "not measured on this connection", never "therefore it is the shell's
|
|
450
|
+
* own version", which is the mistake that made an app bundle older than the shell
|
|
451
|
+
* report a verdict from a build that did not produce it.
|
|
452
|
+
*/
|
|
453
|
+
private helloVersion;
|
|
454
|
+
get reportedEngineVersion(): string | null;
|
|
442
455
|
/**
|
|
443
456
|
* Sink for facts that cannot be attached to any request of the caller: a
|
|
444
457
|
* late reply, a frame with an id this client never issued, a daemon that
|
package/dist/errors.d.ts
CHANGED
|
@@ -81,4 +81,5 @@ export interface ToolErrorText {
|
|
|
81
81
|
*/
|
|
82
82
|
export declare function formatToolError(code: string, message: string, remedy: string): string;
|
|
83
83
|
export declare function formatToolErrorShape(error: ToolErrorText): string;
|
|
84
|
+
export declare function quoteShellArg(value: string): string;
|
|
84
85
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/http-gateway-cli.js
CHANGED
|
@@ -189,13 +189,17 @@ function formatToolError(code, message, remedy) {
|
|
|
189
189
|
function formatToolErrorShape(error) {
|
|
190
190
|
return formatToolError(error.code, error.message, error.remedy);
|
|
191
191
|
}
|
|
192
|
+
var SHELL_INERT_WORD = /^[A-Za-z0-9@%+=:,./_-]+$/;
|
|
193
|
+
function quoteShellArg(value) {
|
|
194
|
+
return SHELL_INERT_WORD.test(value) && !value.startsWith("-") ? value : `'${value.split("'").join("'\\''")}'`;
|
|
195
|
+
}
|
|
192
196
|
|
|
193
197
|
// src/engine-client.ts
|
|
194
198
|
function daemonUnreachableRemedy() {
|
|
195
199
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
196
200
|
const installerCli = path.resolve(here, "..", "..", "installer", "cli.js");
|
|
197
201
|
if (fs2.existsSync(installerCli)) {
|
|
198
|
-
return `run
|
|
202
|
+
return `run \`node ${quoteShellArg(installerCli)} --restore-launchd\` (auto-restores and verifies the launchd-managed daemon), then retry`;
|
|
199
203
|
}
|
|
200
204
|
return "ensure the glasspane daemon is running (launchd job com.glasspane.daemon; in a repo checkout run `node installer/cli.js --restore-launchd`), then retry";
|
|
201
205
|
}
|
|
@@ -452,6 +456,21 @@ var EngineJsonRpcClient = class {
|
|
|
452
456
|
get reportedStateRoot() {
|
|
453
457
|
return this.helloStateRoot;
|
|
454
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* The version the daemon named in its own `hello`, or null when no handshake on this
|
|
461
|
+
* connection has reported one.
|
|
462
|
+
*
|
|
463
|
+
* This is the *producer's* measurement, and it is what the provider attribution line
|
|
464
|
+
* states (`providers.ts`'s `attributeCapability`): the verdict an agent is being told
|
|
465
|
+
* about came out of the daemon's build, not this package's. Read rather than assumed —
|
|
466
|
+
* `null` means "not measured on this connection", never "therefore it is the shell's
|
|
467
|
+
* own version", which is the mistake that made an app bundle older than the shell
|
|
468
|
+
* report a verdict from a build that did not produce it.
|
|
469
|
+
*/
|
|
470
|
+
helloVersion = null;
|
|
471
|
+
get reportedEngineVersion() {
|
|
472
|
+
return this.helloVersion;
|
|
473
|
+
}
|
|
455
474
|
/**
|
|
456
475
|
* Sink for facts that cannot be attached to any request of the caller: a
|
|
457
476
|
* late reply, a frame with an id this client never issued, a daemon that
|
|
@@ -828,6 +847,10 @@ var EngineJsonRpcClient = class {
|
|
|
828
847
|
return;
|
|
829
848
|
}
|
|
830
849
|
const report = result;
|
|
850
|
+
const version = typeof report.version === "string" ? report.version.trim() : "";
|
|
851
|
+
if (version !== "") {
|
|
852
|
+
this.helloVersion = version;
|
|
853
|
+
}
|
|
831
854
|
const protocol = typeof report.protocolVersion === "string" ? report.protocolVersion : null;
|
|
832
855
|
if (protocol === null) {
|
|
833
856
|
this.report(`engine self-report has no protocolVersion (engine=${jsonOrString(report.engine)}); expected '${expected}' \u2014 compatibility unverified`);
|
|
@@ -837,14 +860,14 @@ var EngineJsonRpcClient = class {
|
|
|
837
860
|
this.report(`protocol mismatch: engine speaks '${protocol}', this shell speaks '${expected}' \u2014 upgrade or restart one side before trusting tool results`);
|
|
838
861
|
return;
|
|
839
862
|
}
|
|
840
|
-
const
|
|
863
|
+
const reportedVersion = version === "" ? null : version;
|
|
841
864
|
const reportedRoot = typeof report.stateRoot === "string" ? report.stateRoot.trim() : "";
|
|
842
865
|
if (reportedRoot !== "") {
|
|
843
866
|
this.helloStateRoot = reportedRoot;
|
|
844
867
|
}
|
|
845
868
|
const wantVersion = this.identity?.version;
|
|
846
|
-
if (
|
|
847
|
-
this.report(`version mismatch: engine self-reports '${
|
|
869
|
+
if (reportedVersion !== null && wantVersion !== void 0 && reportedVersion !== wantVersion) {
|
|
870
|
+
this.report(`version mismatch: engine self-reports '${reportedVersion}', this MCP shell ships '${wantVersion}' (single version line) \u2014 one side is stale`);
|
|
848
871
|
}
|
|
849
872
|
if (this.expectedDaemonMethods.length > 0) {
|
|
850
873
|
const advertised = Array.isArray(report.capabilities) ? report.capabilities.filter((item) => typeof item === "string") : null;
|
|
@@ -1652,8 +1675,65 @@ var ProviderRegistrationSchema = z8.strictObject({
|
|
|
1652
1675
|
seen.add(method.name);
|
|
1653
1676
|
});
|
|
1654
1677
|
});
|
|
1678
|
+
var PROVIDER_CODES = {
|
|
1679
|
+
duplicateId: "KERNEL_E_PROVIDER_DUPLICATE",
|
|
1680
|
+
unknown: "KERNEL_E_PROVIDER_UNKNOWN",
|
|
1681
|
+
missingMethod: "KERNEL_E_PROVIDER_METHOD"
|
|
1682
|
+
};
|
|
1683
|
+
var KernelProviderError = class extends Error {
|
|
1684
|
+
code;
|
|
1685
|
+
providerId;
|
|
1686
|
+
remedy;
|
|
1687
|
+
constructor(code, providerId, message, remedy) {
|
|
1688
|
+
super(`${code}: ${message}`);
|
|
1689
|
+
this.name = "KernelProviderError";
|
|
1690
|
+
this.code = code;
|
|
1691
|
+
this.providerId = providerId;
|
|
1692
|
+
this.remedy = remedy;
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
function emptyProviderRegistry() {
|
|
1696
|
+
return {};
|
|
1697
|
+
}
|
|
1698
|
+
function registeredIds(registry) {
|
|
1699
|
+
const ids = Object.keys(registry).sort();
|
|
1700
|
+
return ids.length > 0 ? ids.join(", ") : "(none registered)";
|
|
1701
|
+
}
|
|
1702
|
+
function registerProvider(registry, provider) {
|
|
1703
|
+
const checked = ProviderRegistrationSchema.safeParse(provider);
|
|
1704
|
+
if (!checked.success) {
|
|
1705
|
+
throw schemaErrorFrom("provider registration", checked.error);
|
|
1706
|
+
}
|
|
1707
|
+
const parsed = checked.data;
|
|
1708
|
+
if (Object.prototype.hasOwnProperty.call(registry, parsed.id)) {
|
|
1709
|
+
throw new KernelProviderError(PROVIDER_CODES.duplicateId, parsed.id, `provider "${parsed.id}" is already registered`, "choose a distinct id, or replace the registration explicitly rather than by order of appearance");
|
|
1710
|
+
}
|
|
1711
|
+
return { ...registry, [parsed.id]: parsed };
|
|
1712
|
+
}
|
|
1713
|
+
function resolveProvider(registry, id) {
|
|
1714
|
+
const provider = registry[id];
|
|
1715
|
+
if (!provider) {
|
|
1716
|
+
throw new KernelProviderError(PROVIDER_CODES.unknown, id, `no provider registered under "${id}"`, `registered: ${registeredIds(registry)}`);
|
|
1717
|
+
}
|
|
1718
|
+
return provider;
|
|
1719
|
+
}
|
|
1720
|
+
function requireMethod(registry, id, method) {
|
|
1721
|
+
const provider = resolveProvider(registry, id);
|
|
1722
|
+
const found = provider.methods.find((entry) => entry.name === method);
|
|
1723
|
+
if (!found) {
|
|
1724
|
+
throw new KernelProviderError(PROVIDER_CODES.missingMethod, id, `provider "${id}" does not offer "${method}"`, `it offers: ${provider.methods.map((entry) => entry.name).join(", ")}`);
|
|
1725
|
+
}
|
|
1726
|
+
return found;
|
|
1727
|
+
}
|
|
1655
1728
|
|
|
1656
1729
|
// ../node_modules/iterate-kernel/dist/parse.js
|
|
1730
|
+
function parseOrThrow(schema, input, label) {
|
|
1731
|
+
const result = schema.safeParse(input);
|
|
1732
|
+
if (result.success) {
|
|
1733
|
+
return result.data;
|
|
1734
|
+
}
|
|
1735
|
+
throw schemaErrorFrom(label, result.error);
|
|
1736
|
+
}
|
|
1657
1737
|
function parseEvidencePackRead(input) {
|
|
1658
1738
|
const result = EvidencePackReadSchema.safeParse(input);
|
|
1659
1739
|
if (result.success) {
|
|
@@ -1661,6 +1741,9 @@ function parseEvidencePackRead(input) {
|
|
|
1661
1741
|
}
|
|
1662
1742
|
throw schemaErrorFrom("evidence pack", result.error);
|
|
1663
1743
|
}
|
|
1744
|
+
function parseProviderRegistration(input) {
|
|
1745
|
+
return parseOrThrow(ProviderRegistrationSchema, input, "provider registration");
|
|
1746
|
+
}
|
|
1664
1747
|
|
|
1665
1748
|
// src/canonical.ts
|
|
1666
1749
|
function canonicalJson2(value) {
|
|
@@ -1681,6 +1764,169 @@ function sortKeys(value) {
|
|
|
1681
1764
|
return value;
|
|
1682
1765
|
}
|
|
1683
1766
|
|
|
1767
|
+
// src/providers.ts
|
|
1768
|
+
import fs3 from "node:fs";
|
|
1769
|
+
import path2 from "node:path";
|
|
1770
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1771
|
+
var GLASSPANE_AE_PROVIDER_ID = "glasspane-engine-ae";
|
|
1772
|
+
var GLASSPANE_EVIDENCE_PROVIDER_ID = "glasspane-evidence";
|
|
1773
|
+
var MANIFEST_NAME = "glasspane-mcp";
|
|
1774
|
+
var cachedProductVersion;
|
|
1775
|
+
function shellManifestPath() {
|
|
1776
|
+
return path2.resolve(path2.dirname(fileURLToPath2(import.meta.url)), "..", "package.json");
|
|
1777
|
+
}
|
|
1778
|
+
function shellProductVersion() {
|
|
1779
|
+
if (cachedProductVersion === void 0) {
|
|
1780
|
+
cachedProductVersion = readShellProductVersion();
|
|
1781
|
+
}
|
|
1782
|
+
return cachedProductVersion;
|
|
1783
|
+
}
|
|
1784
|
+
function readShellProductVersion() {
|
|
1785
|
+
let raw;
|
|
1786
|
+
try {
|
|
1787
|
+
raw = fs3.readFileSync(shellManifestPath(), "utf8");
|
|
1788
|
+
} catch {
|
|
1789
|
+
return null;
|
|
1790
|
+
}
|
|
1791
|
+
let parsed;
|
|
1792
|
+
try {
|
|
1793
|
+
parsed = JSON.parse(raw);
|
|
1794
|
+
} catch {
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1800
|
+
const record = parsed;
|
|
1801
|
+
if (record.name !== MANIFEST_NAME) {
|
|
1802
|
+
return null;
|
|
1803
|
+
}
|
|
1804
|
+
return typeof record.version === "string" ? record.version : null;
|
|
1805
|
+
}
|
|
1806
|
+
function shellProviderDrafts(version) {
|
|
1807
|
+
return [
|
|
1808
|
+
{
|
|
1809
|
+
schemaVersion: PROVIDER_SCHEMA_VERSION,
|
|
1810
|
+
id: GLASSPANE_AE_PROVIDER_ID,
|
|
1811
|
+
kind: "ae-diagnosis",
|
|
1812
|
+
version,
|
|
1813
|
+
origin: "glasspane",
|
|
1814
|
+
methods: [
|
|
1815
|
+
{ name: "diagnose", requires: ["ax-event", "attribution", "circuit-breaker"] }
|
|
1816
|
+
]
|
|
1817
|
+
},
|
|
1818
|
+
{
|
|
1819
|
+
schemaVersion: PROVIDER_SCHEMA_VERSION,
|
|
1820
|
+
id: GLASSPANE_EVIDENCE_PROVIDER_ID,
|
|
1821
|
+
kind: "evidence-source",
|
|
1822
|
+
version,
|
|
1823
|
+
origin: "glasspane",
|
|
1824
|
+
methods: [
|
|
1825
|
+
{ name: "capture", requires: ["ax-tree"] },
|
|
1826
|
+
{ name: "last-evidence", requires: [] }
|
|
1827
|
+
]
|
|
1828
|
+
}
|
|
1829
|
+
];
|
|
1830
|
+
}
|
|
1831
|
+
function shellProviderRegistry(version) {
|
|
1832
|
+
return shellProviderDrafts(version).reduce(
|
|
1833
|
+
(registry, draft) => registerProvider(registry, parseProviderRegistration(draft)),
|
|
1834
|
+
emptyProviderRegistry()
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
function coveredMethods(signals, registry) {
|
|
1838
|
+
const present = /* @__PURE__ */ new Set();
|
|
1839
|
+
for (const name of signals) {
|
|
1840
|
+
present.add(name);
|
|
1841
|
+
}
|
|
1842
|
+
const provider = resolveProvider(registry, GLASSPANE_AE_PROVIDER_ID);
|
|
1843
|
+
return provider.methods.filter((method) => method.requires.every((need) => present.has(need)));
|
|
1844
|
+
}
|
|
1845
|
+
var CAPABILITY_CLAIMS = [
|
|
1846
|
+
{ engineMethod: "diagnose", providerId: GLASSPANE_AE_PROVIDER_ID, method: "diagnose" }
|
|
1847
|
+
];
|
|
1848
|
+
var CLAIMED_ENGINE_METHODS = CAPABILITY_CLAIMS.map((claim) => claim.engineMethod);
|
|
1849
|
+
function claimForEngineMethod(engineMethod) {
|
|
1850
|
+
const found = CAPABILITY_CLAIMS.filter((claim) => claim.engineMethod === engineMethod);
|
|
1851
|
+
const only = found.length === 1 ? found[0] : void 0;
|
|
1852
|
+
return only === void 0 ? null : only;
|
|
1853
|
+
}
|
|
1854
|
+
function attributeCapability(engineMethod, query = {}) {
|
|
1855
|
+
const claim = claimForEngineMethod(engineMethod);
|
|
1856
|
+
if (claim === null) {
|
|
1857
|
+
return { status: "not-claimed" };
|
|
1858
|
+
}
|
|
1859
|
+
const measured = typeof query.producerVersion === "string" && query.producerVersion.trim() !== "" ? query.producerVersion.trim() : null;
|
|
1860
|
+
const own = query.shellVersion === void 0 ? shellProductVersion() : query.shellVersion;
|
|
1861
|
+
const usable = (value) => value !== null && value.trim() !== "" ? value.trim() : null;
|
|
1862
|
+
const registrationVersion = measured ?? usable(own) ?? UNSTATED_REGISTRATION_VERSION;
|
|
1863
|
+
let target = query.registry;
|
|
1864
|
+
try {
|
|
1865
|
+
if (target === void 0) {
|
|
1866
|
+
target = shellProviderRegistry(registrationVersion);
|
|
1867
|
+
}
|
|
1868
|
+
const declared = requireMethod(target, claim.providerId, claim.method);
|
|
1869
|
+
const provider = resolveProvider(target, claim.providerId);
|
|
1870
|
+
const covered = coveredMethods(declared.requires, target);
|
|
1871
|
+
const only = covered.length === 1 ? covered[0] : void 0;
|
|
1872
|
+
if (only === void 0 || only.name !== claim.method) {
|
|
1873
|
+
return { status: "refused", error: coverageAmbiguous(claim, covered) };
|
|
1874
|
+
}
|
|
1875
|
+
return {
|
|
1876
|
+
status: "ok",
|
|
1877
|
+
provider: {
|
|
1878
|
+
// Never `provider.version`: that field is whatever the registration was built
|
|
1879
|
+
// with, which can be the placeholder. The version an agent is told is the one the
|
|
1880
|
+
// producer itself reported.
|
|
1881
|
+
id: provider.id,
|
|
1882
|
+
version: measured,
|
|
1883
|
+
method: declared.name,
|
|
1884
|
+
requires: declared.requires,
|
|
1885
|
+
versionNote: versionNote(measured, usable(own))
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
if (error instanceof KernelProviderError) {
|
|
1890
|
+
return { status: "refused", error: { code: error.code, message: error.message, remedy: error.remedy } };
|
|
1891
|
+
}
|
|
1892
|
+
if (error instanceof KernelSchemaError) {
|
|
1893
|
+
return {
|
|
1894
|
+
status: "refused",
|
|
1895
|
+
error: {
|
|
1896
|
+
code: error.code,
|
|
1897
|
+
message: `${error.message} \u2014 the kernel refused a registration this product writes itself, built with the version from ${registrationVersionSource(measured, usable(own))}`,
|
|
1898
|
+
remedy: "fix shellProviderDrafts in mcp-shell/src/providers.ts; it is the contract both harnesses read, so it cannot be relaxed at a call site \u2014 run node scripts/check-kernel-corpus.mjs and compare against kernel/fixtures/provider-registration.ok-01.json"
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
throw error;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
var UNSTATED_REGISTRATION_VERSION = "unmeasured-registration-placeholder";
|
|
1906
|
+
function registrationVersionSource(measured, own) {
|
|
1907
|
+
if (measured !== null) {
|
|
1908
|
+
return "the daemon's own hello self-report";
|
|
1909
|
+
}
|
|
1910
|
+
return own === null ? `the ${MANIFEST_NAME} manifest this build cannot read at ${shellManifestPath()}` : "this build's own manifest";
|
|
1911
|
+
}
|
|
1912
|
+
function providerAttributionLine(attributed) {
|
|
1913
|
+
const needs = attributed.requires.length > 0 ? attributed.requires.join(", ") : "none";
|
|
1914
|
+
const version = attributed.version === null ? "version unmeasured" : `version ${attributed.version}`;
|
|
1915
|
+
return `provider: ${attributed.id} ${version} method ${attributed.method} | declared prerequisites: ${needs} | ${attributed.versionNote} | the verdict above is the engine's, transcribed unchanged; this line names the registered producer this surface attributes it to, from the kernel's provider-registration contract`;
|
|
1916
|
+
}
|
|
1917
|
+
function versionNote(measured, own) {
|
|
1918
|
+
const producer = measured === null ? "the producer's version is unmeasured: no daemon hello handshake has been answered on this connection, so this line states no version" : "the producer's version is the daemon's own hello self-report, read on this connection";
|
|
1919
|
+
return own === null ? `${producer}; this build cannot read its own ${MANIFEST_NAME} manifest at ${shellManifestPath()}, so it states no version of itself either` : producer;
|
|
1920
|
+
}
|
|
1921
|
+
function coverageAmbiguous(claim, covered) {
|
|
1922
|
+
const names = covered.map((method) => method.name).join(", ");
|
|
1923
|
+
return {
|
|
1924
|
+
code: GP_E_INTERNAL,
|
|
1925
|
+
message: `the prerequisites declared for '${claim.method}' cover ${covered.length === 0 ? "no" : `${covered.length} parallel`} method${covered.length === 1 ? "" : "s"} of provider ${claim.providerId}${names === "" ? "" : ` (${names})`}`,
|
|
1926
|
+
remedy: "give each declared method a prerequisite list that tells it apart from the others, or ask for the method by the one name the contract distinguishes \u2014 naming a verdict's producer by guessing which of two indistinguishable declarations ran is the default resolution this contract forbids"
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1684
1930
|
// src/update-state.ts
|
|
1685
1931
|
var CA_ROOTS_STATUSES = ["ok", "probe-failed", "empty", "unavailable", "write-unverified"];
|
|
1686
1932
|
var KNOWN_CA_ROOTS_STATUSES = new Set(CA_ROOTS_STATUSES);
|
|
@@ -1696,9 +1942,9 @@ var TRAIL_PROVENANCE_NOTES = {
|
|
|
1696
1942
|
};
|
|
1697
1943
|
|
|
1698
1944
|
// src/project-registry.ts
|
|
1699
|
-
import
|
|
1945
|
+
import fs4 from "node:fs";
|
|
1700
1946
|
import os2 from "node:os";
|
|
1701
|
-
import
|
|
1947
|
+
import path3 from "node:path";
|
|
1702
1948
|
import { randomBytes } from "node:crypto";
|
|
1703
1949
|
import { z as z9 } from "zod";
|
|
1704
1950
|
var CROCKFORD_BODY_RE = "[0-9A-HJKMNP-TV-Z]{26}";
|
|
@@ -1751,8 +1997,8 @@ function systemHome() {
|
|
|
1751
1997
|
}
|
|
1752
1998
|
function daemonProjectsPath(daemonStateRoot) {
|
|
1753
1999
|
const named2 = typeof daemonStateRoot === "string" ? daemonStateRoot.trim() : "";
|
|
1754
|
-
if (named2 !== "") return
|
|
1755
|
-
return
|
|
2000
|
+
if (named2 !== "") return path3.join(named2, "projects.json");
|
|
2001
|
+
return path3.join(systemHome(), ".glasspane", "projects.json");
|
|
1756
2002
|
}
|
|
1757
2003
|
var FIRMLINK_PREFIX = "/System/Volumes/Data";
|
|
1758
2004
|
var FORCE_OVERWRITE_ENV = "GLASSPANE_PROJECTS_FORCE_OVERWRITE";
|
|
@@ -1783,7 +2029,7 @@ function projectsPath(daemonStateRoot) {
|
|
|
1783
2029
|
function loadRegistry(filePath) {
|
|
1784
2030
|
let raw;
|
|
1785
2031
|
try {
|
|
1786
|
-
raw =
|
|
2032
|
+
raw = fs4.readFileSync(filePath, "utf8");
|
|
1787
2033
|
} catch (error) {
|
|
1788
2034
|
if (nodeErrorCode(error) === "ENOENT") {
|
|
1789
2035
|
return { projects: [], failure: null };
|
|
@@ -1852,7 +2098,7 @@ function requireUsableRegistry(filePath, snapshot) {
|
|
|
1852
2098
|
}
|
|
1853
2099
|
const discarded = `${filePath}.unreadable-${uniqueToken()}`;
|
|
1854
2100
|
try {
|
|
1855
|
-
|
|
2101
|
+
fs4.renameSync(filePath, discarded);
|
|
1856
2102
|
} catch (error) {
|
|
1857
2103
|
throw new ProjectRegistryError(
|
|
1858
2104
|
GP_E_INTERNAL,
|
|
@@ -1862,7 +2108,7 @@ function requireUsableRegistry(filePath, snapshot) {
|
|
|
1862
2108
|
return { projects: [], discarded };
|
|
1863
2109
|
}
|
|
1864
2110
|
function registryReaders(target) {
|
|
1865
|
-
return `\`/usr/bin/plutil -p ${target}\` or \`/bin/cat ${target}\` \u2014 both absolute, so they run from whatever directory the reader is in and need nothing that may be off PATH`;
|
|
2111
|
+
return `\`/usr/bin/plutil -p ${quoteShellArg(target)}\` or \`/bin/cat ${quoteShellArg(target)}\` \u2014 both absolute, so they run from whatever directory the reader is in and need nothing that may be off PATH`;
|
|
1866
2112
|
}
|
|
1867
2113
|
function unreadableMessage(filePath, failure) {
|
|
1868
2114
|
return `projects.json at ${filePath} ${failure}. gp_project_set refused to write it: replacing an unreadable registry discards every registration in it. See the damage with ${registryReaders(filePath)} and repair that file, or set ${FORCE_OVERWRITE_ENV}=1 and retry to rebuild the registry from scratch \u2014 the unloadable file is moved aside as ${filePath}.unreadable-<id> rather than deleted, and every existing project entry is lost.`;
|
|
@@ -1874,7 +2120,7 @@ var STATE_DIR_MODE = 448;
|
|
|
1874
2120
|
var STATE_FILE_MODE = 384;
|
|
1875
2121
|
function tightenMode(target, want, kind) {
|
|
1876
2122
|
try {
|
|
1877
|
-
|
|
2123
|
+
fs4.chmodSync(target, want);
|
|
1878
2124
|
} catch (error) {
|
|
1879
2125
|
throw new ProjectRegistryError(
|
|
1880
2126
|
GP_E_INTERNAL,
|
|
@@ -1883,7 +2129,7 @@ function tightenMode(target, want, kind) {
|
|
|
1883
2129
|
}
|
|
1884
2130
|
let mode;
|
|
1885
2131
|
try {
|
|
1886
|
-
mode =
|
|
2132
|
+
mode = fs4.statSync(target).mode & 511;
|
|
1887
2133
|
} catch (error) {
|
|
1888
2134
|
throw new ProjectRegistryError(
|
|
1889
2135
|
GP_E_INTERNAL,
|
|
@@ -1899,7 +2145,7 @@ function tightenMode(target, want, kind) {
|
|
|
1899
2145
|
}
|
|
1900
2146
|
function fileIdentity(filePath) {
|
|
1901
2147
|
try {
|
|
1902
|
-
const stat =
|
|
2148
|
+
const stat = fs4.statSync(filePath);
|
|
1903
2149
|
return `${stat.mtimeMs}:${stat.size}:${stat.ino}`;
|
|
1904
2150
|
} catch (error) {
|
|
1905
2151
|
if (nodeErrorCode(error) === "ENOENT") return null;
|
|
@@ -1921,47 +2167,47 @@ function assertNoConcurrentWrite(filePath, openedAs) {
|
|
|
1921
2167
|
function syncDirectory(dir) {
|
|
1922
2168
|
let dirFd = null;
|
|
1923
2169
|
try {
|
|
1924
|
-
dirFd =
|
|
1925
|
-
|
|
2170
|
+
dirFd = fs4.openSync(dir, "r");
|
|
2171
|
+
fs4.fsyncSync(dirFd);
|
|
1926
2172
|
} catch (error) {
|
|
1927
2173
|
const code = error?.code;
|
|
1928
2174
|
if (code === "EIO" || code === "ENOMEM") throw error;
|
|
1929
2175
|
} finally {
|
|
1930
|
-
if (dirFd !== null)
|
|
2176
|
+
if (dirFd !== null) fs4.closeSync(dirFd);
|
|
1931
2177
|
}
|
|
1932
2178
|
}
|
|
1933
2179
|
function saveProjects(filePath, entries) {
|
|
1934
|
-
const dir =
|
|
2180
|
+
const dir = path3.dirname(filePath);
|
|
1935
2181
|
const tmpPath = `${filePath}.tmp-${uniqueToken()}`;
|
|
1936
2182
|
try {
|
|
1937
|
-
|
|
2183
|
+
fs4.mkdirSync(dir, { recursive: true, mode: STATE_DIR_MODE });
|
|
1938
2184
|
tightenMode(dir, STATE_DIR_MODE, "directory");
|
|
1939
|
-
const fd =
|
|
2185
|
+
const fd = fs4.openSync(tmpPath, "w", STATE_FILE_MODE);
|
|
1940
2186
|
try {
|
|
1941
|
-
|
|
1942
|
-
|
|
2187
|
+
fs4.writeFileSync(fd, JSON.stringify(entries, null, 2), "utf8");
|
|
2188
|
+
fs4.fsyncSync(fd);
|
|
1943
2189
|
} finally {
|
|
1944
|
-
|
|
2190
|
+
fs4.closeSync(fd);
|
|
1945
2191
|
}
|
|
1946
2192
|
tightenMode(tmpPath, STATE_FILE_MODE, "file");
|
|
1947
|
-
|
|
2193
|
+
fs4.renameSync(tmpPath, filePath);
|
|
1948
2194
|
syncDirectory(dir);
|
|
1949
2195
|
tightenMode(filePath, STATE_FILE_MODE, "file");
|
|
1950
2196
|
} catch (error) {
|
|
1951
2197
|
try {
|
|
1952
|
-
|
|
2198
|
+
fs4.unlinkSync(tmpPath);
|
|
1953
2199
|
} catch {
|
|
1954
2200
|
}
|
|
1955
2201
|
if (error instanceof ProjectRegistryError) {
|
|
1956
2202
|
throw new ProjectRegistryError(
|
|
1957
2203
|
error.code,
|
|
1958
|
-
`${error.message}; projects.json at ${filePath} was therefore not published as owner-only, and gp_project_set refuses to report a write the daemon's own modes did not hold. Bring the state root to them yourself (chmod 700 ${dir}, chmod 600 ${filePath}) and retry \u2014 a volume that ignores chmod (read-only mount, ACL, immutable flag) needs the daemon's --state-dir moved to one that honours permissions; until then the registry on disk and this shell disagree about who can read it`
|
|
2204
|
+
`${error.message}; projects.json at ${filePath} was therefore not published as owner-only, and gp_project_set refuses to report a write the daemon's own modes did not hold. Bring the state root to them yourself (chmod 700 ${quoteShellArg(dir)}, chmod 600 ${quoteShellArg(filePath)}) and retry \u2014 a volume that ignores chmod (read-only mount, ACL, immutable flag) needs the daemon's --state-dir moved to one that honours permissions; until then the registry on disk and this shell disagree about who can read it`
|
|
1959
2205
|
);
|
|
1960
2206
|
}
|
|
1961
2207
|
const errno = error?.code ?? "unknown";
|
|
1962
2208
|
throw new ProjectRegistryError(
|
|
1963
2209
|
GP_E_INTERNAL,
|
|
1964
|
-
`projects.json at ${filePath} was not published: committing the bytes to the device failed (${errno}). gp_project_set answers "not saved" here rather than "saved", because a registration a power loss can drop is not a registration. This is the write's own failure, not a verdict about the file's contents: check what the volume will do (\`ls -lO ${dir}\` for an immutable flag, \`diskutil info -live ${dir}\` for a read-only or remote mount), fix that, then retry the same call.`
|
|
2210
|
+
`projects.json at ${filePath} was not published: committing the bytes to the device failed (${errno}). gp_project_set answers "not saved" here rather than "saved", because a registration a power loss can drop is not a registration. This is the write's own failure, not a verdict about the file's contents: check what the volume will do (\`ls -lO ${quoteShellArg(dir)}\` for an immutable flag, \`diskutil info -live ${quoteShellArg(dir)}\` for a read-only or remote mount), fix that, then retry the same call.`
|
|
1965
2211
|
);
|
|
1966
2212
|
}
|
|
1967
2213
|
}
|
|
@@ -2065,7 +2311,7 @@ function daemonRestartNotice(filePath, daemonStateRoot) {
|
|
|
2065
2311
|
const named2 = typeof daemonStateRoot === "string" && daemonStateRoot.trim() !== "" ? daemonStateRoot.trim() : null;
|
|
2066
2312
|
const daemonPath = daemonProjectsPath(named2);
|
|
2067
2313
|
const authority = named2 === null ? "its default root is the folder under this user's home record (where a service started with no --state-dir looks)" : `its root is the one the running daemon named in its hello (${named2})`;
|
|
2068
|
-
if (
|
|
2314
|
+
if (path3.resolve(filePath) !== path3.resolve(daemonPath)) {
|
|
2069
2315
|
return `Saved in ${filePath}. The background service does not read this path because of anything in its own configuration: ${authority}. It loads ${daemonPath} once when it starts, and ${PROJECTS_FILE_ENV} \u2014 the variable that pointed this write here \u2014 is honoured by this MCP shell only, so as far as the service's own configuration goes it never reads that file, and no restart of the service will make it load this one. ${rootDiscriminator()} To land the entry where that root looks: unset ${PROJECTS_FILE_ENV} for this shell (or point it at ${daemonPath}) and repeat gp_project_set, then restart the background service so it reloads \u2014 ${restartCommand()}`;
|
|
2070
2316
|
}
|
|
2071
2317
|
if (named2 !== null) {
|
|
@@ -2100,7 +2346,7 @@ function checkAgentPath(field, value) {
|
|
|
2100
2346
|
throw badPath(field, value, `must not contain "." or ".." components (got "${value}")`);
|
|
2101
2347
|
}
|
|
2102
2348
|
}
|
|
2103
|
-
const logical =
|
|
2349
|
+
const logical = path3.posix.normalize(value);
|
|
2104
2350
|
const given = stripFirmlink(logical);
|
|
2105
2351
|
const resolved = stripFirmlink(resolveExistingAncestor(logical));
|
|
2106
2352
|
const refusal = strongestRefusal(given === resolved ? [resolved] : [resolved, given], resolved);
|
|
@@ -2149,7 +2395,7 @@ function refuseHomeDotDirectory(target, home) {
|
|
|
2149
2395
|
return named(`a dot-directory of the current user's home directory (~/${first}), which is where that account's credentials and client configuration live (\`~/.ssh\`, \`~/.aws\`, \`~/.gnupg\`, \`~/.config\`) or the daemon's own private state (\`~/.glasspane\`); the daemon chmods a registered storage root to 0700 and writes evidence packs into it, and prunes expired ones out of it, so this is not a place a project archive may be pointed at \u2014 name a directory inside the project instead, e.g. "the project tree/${first}"`);
|
|
2150
2396
|
}
|
|
2151
2397
|
function refuseCandidate(target) {
|
|
2152
|
-
const home = stripFirmlink(
|
|
2398
|
+
const home = stripFirmlink(path3.posix.normalize(systemHome()));
|
|
2153
2399
|
if (target === home) return named("the current user's home directory");
|
|
2154
2400
|
if (isInside(`${home}/Library`, target)) return named("inside the current user's Library folder");
|
|
2155
2401
|
const homeDot = refuseHomeDotDirectory(target, home);
|
|
@@ -2178,7 +2424,7 @@ function refuseUnownedTree(target) {
|
|
|
2178
2424
|
}
|
|
2179
2425
|
let stat;
|
|
2180
2426
|
try {
|
|
2181
|
-
stat =
|
|
2427
|
+
stat = fs4.statSync(ancestor);
|
|
2182
2428
|
} catch (error) {
|
|
2183
2429
|
return `under a nearest existing directory ${ancestor} that cannot be inspected (${describeError(error)})`;
|
|
2184
2430
|
}
|
|
@@ -2193,10 +2439,10 @@ function refuseUnownedTree(target) {
|
|
|
2193
2439
|
function nearestExistingAncestor(target) {
|
|
2194
2440
|
let cursor = target;
|
|
2195
2441
|
for (; ; ) {
|
|
2196
|
-
if (
|
|
2442
|
+
if (fs4.existsSync(cursor)) {
|
|
2197
2443
|
return cursor;
|
|
2198
2444
|
}
|
|
2199
|
-
const parent =
|
|
2445
|
+
const parent = path3.posix.dirname(cursor);
|
|
2200
2446
|
if (parent === cursor) return parent;
|
|
2201
2447
|
cursor = parent;
|
|
2202
2448
|
}
|
|
@@ -2216,12 +2462,12 @@ function resolveExistingAncestor(target) {
|
|
|
2216
2462
|
const missingTail = [];
|
|
2217
2463
|
for (; ; ) {
|
|
2218
2464
|
try {
|
|
2219
|
-
const real =
|
|
2220
|
-
return missingTail.length === 0 ? real :
|
|
2465
|
+
const real = fs4.realpathSync(cursor);
|
|
2466
|
+
return missingTail.length === 0 ? real : path3.posix.join(real, ...missingTail);
|
|
2221
2467
|
} catch {
|
|
2222
|
-
const parent =
|
|
2468
|
+
const parent = path3.posix.dirname(cursor);
|
|
2223
2469
|
if (parent === cursor) return target;
|
|
2224
|
-
missingTail.unshift(
|
|
2470
|
+
missingTail.unshift(path3.posix.basename(cursor));
|
|
2225
2471
|
cursor = parent;
|
|
2226
2472
|
}
|
|
2227
2473
|
}
|
|
@@ -3707,6 +3953,14 @@ async function handleToolForward(name, req, client, res) {
|
|
|
3707
3953
|
);
|
|
3708
3954
|
return;
|
|
3709
3955
|
}
|
|
3956
|
+
const claim = attributeCapability(spec.engineMethod, {
|
|
3957
|
+
producerVersion: client.reportedEngineVersion ?? null
|
|
3958
|
+
});
|
|
3959
|
+
if (claim.status === "refused") {
|
|
3960
|
+
fail(res, httpStatusForEngineCode(claim.error.code), claim.error.code, claim.error.message, claim.error.remedy);
|
|
3961
|
+
return;
|
|
3962
|
+
}
|
|
3963
|
+
const attribution = claim.status === "ok" ? providerAttributionLine(claim.provider) : void 0;
|
|
3710
3964
|
try {
|
|
3711
3965
|
const result = await client.call(spec.engineMethod, checked.value);
|
|
3712
3966
|
if (spec.engineMethod === "last_evidence") {
|
|
@@ -3717,7 +3971,7 @@ async function handleToolForward(name, req, client, res) {
|
|
|
3717
3971
|
return;
|
|
3718
3972
|
}
|
|
3719
3973
|
}
|
|
3720
|
-
ok(res, result);
|
|
3974
|
+
ok(res, result, attribution);
|
|
3721
3975
|
} catch (error) {
|
|
3722
3976
|
await failEngine(res, error, spec, checked.value);
|
|
3723
3977
|
}
|
|
@@ -3824,8 +4078,8 @@ function makeNotFetchedNote(notFetched, format) {
|
|
|
3824
4078
|
const sentence = `not fetched within this request's ${EVIDENCE_FANOUT_BUDGET_MS}ms wait: ${notFetched.length} ${label} left unanswered: ${notFetched.join(", ")}. Fetch one by id with GET /v1/evidence/<operationId>, or ask again with a shorter ids= list`;
|
|
3825
4079
|
return format === "html" ? `<p class="gp-not-fetched">${escapeHTML(sentence)}</p>` : `> ${sentence}`;
|
|
3826
4080
|
}
|
|
3827
|
-
function ok(res, result) {
|
|
3828
|
-
writeJson(res, 200, { ok: true, result });
|
|
4081
|
+
function ok(res, result, attribution) {
|
|
4082
|
+
writeJson(res, 200, attribution === void 0 ? { ok: true, result } : { ok: true, result, attribution });
|
|
3829
4083
|
}
|
|
3830
4084
|
function fail(res, status, code, message, remedy) {
|
|
3831
4085
|
writeJson(res, status, { ok: false, error: { code, message: forHttpCaller(message), remedy: forHttpCaller(remedy) } });
|
package/dist/http-gateway.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export interface EngineClientLike {
|
|
|
75
75
|
method: string;
|
|
76
76
|
result: unknown;
|
|
77
77
|
}) => void): void;
|
|
78
|
+
/**
|
|
79
|
+
* The version the daemon named in its own `hello` on this connection, or null when no
|
|
80
|
+
* handshake has reported one. Optional so a fake may omit it — the seam then attributes
|
|
81
|
+
* what it can measure and says the version is unmeasured, which is exactly what the
|
|
82
|
+
* stdio doorway does for the same client.
|
|
83
|
+
*/
|
|
84
|
+
readonly reportedEngineVersion?: string | null;
|
|
78
85
|
}
|
|
79
86
|
export interface HttpGatewayOptions {
|
|
80
87
|
/** Daemon socket path handed to the engine client (the default connector). */
|