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.
@@ -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.18.0";
29
+ readonly version: "1.19.0";
30
30
  };
31
31
  export declare const PARSE_ERROR = -32700;
32
32
  export declare const INVALID_REQUEST = -32600;
@@ -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
@@ -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 "node ${installerCli} --restore-launchd" (auto-restores and verifies the launchd-managed daemon), then retry`;
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 version = typeof report.version === "string" ? report.version : null;
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 (version !== null && wantVersion !== void 0 && version !== wantVersion) {
847
- this.report(`version mismatch: engine self-reports '${version}', this MCP shell ships '${wantVersion}' (single version line) \u2014 one side is stale`);
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 fs3 from "node:fs";
1945
+ import fs4 from "node:fs";
1700
1946
  import os2 from "node:os";
1701
- import path2 from "node:path";
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 path2.join(named2, "projects.json");
1755
- return path2.join(systemHome(), ".glasspane", "projects.json");
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 = fs3.readFileSync(filePath, "utf8");
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
- fs3.renameSync(filePath, discarded);
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
- fs3.chmodSync(target, want);
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 = fs3.statSync(target).mode & 511;
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 = fs3.statSync(filePath);
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 = fs3.openSync(dir, "r");
1925
- fs3.fsyncSync(dirFd);
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) fs3.closeSync(dirFd);
2176
+ if (dirFd !== null) fs4.closeSync(dirFd);
1931
2177
  }
1932
2178
  }
1933
2179
  function saveProjects(filePath, entries) {
1934
- const dir = path2.dirname(filePath);
2180
+ const dir = path3.dirname(filePath);
1935
2181
  const tmpPath = `${filePath}.tmp-${uniqueToken()}`;
1936
2182
  try {
1937
- fs3.mkdirSync(dir, { recursive: true, mode: STATE_DIR_MODE });
2183
+ fs4.mkdirSync(dir, { recursive: true, mode: STATE_DIR_MODE });
1938
2184
  tightenMode(dir, STATE_DIR_MODE, "directory");
1939
- const fd = fs3.openSync(tmpPath, "w", STATE_FILE_MODE);
2185
+ const fd = fs4.openSync(tmpPath, "w", STATE_FILE_MODE);
1940
2186
  try {
1941
- fs3.writeFileSync(fd, JSON.stringify(entries, null, 2), "utf8");
1942
- fs3.fsyncSync(fd);
2187
+ fs4.writeFileSync(fd, JSON.stringify(entries, null, 2), "utf8");
2188
+ fs4.fsyncSync(fd);
1943
2189
  } finally {
1944
- fs3.closeSync(fd);
2190
+ fs4.closeSync(fd);
1945
2191
  }
1946
2192
  tightenMode(tmpPath, STATE_FILE_MODE, "file");
1947
- fs3.renameSync(tmpPath, filePath);
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
- fs3.unlinkSync(tmpPath);
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 (path2.resolve(filePath) !== path2.resolve(daemonPath)) {
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 = path2.posix.normalize(value);
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(path2.posix.normalize(systemHome()));
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 = fs3.statSync(ancestor);
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 (fs3.existsSync(cursor)) {
2442
+ if (fs4.existsSync(cursor)) {
2197
2443
  return cursor;
2198
2444
  }
2199
- const parent = path2.posix.dirname(cursor);
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 = fs3.realpathSync(cursor);
2220
- return missingTail.length === 0 ? real : path2.posix.join(real, ...missingTail);
2465
+ const real = fs4.realpathSync(cursor);
2466
+ return missingTail.length === 0 ? real : path3.posix.join(real, ...missingTail);
2221
2467
  } catch {
2222
- const parent = path2.posix.dirname(cursor);
2468
+ const parent = path3.posix.dirname(cursor);
2223
2469
  if (parent === cursor) return target;
2224
- missingTail.unshift(path2.posix.basename(cursor));
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) } });
@@ -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). */