@ashlr/hub 3.4.0 → 3.5.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.
Files changed (213) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +3 -0
  3. package/dist/build-identity.json +2 -2
  4. package/dist/cli/index.js +11 -0
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/cli/local-runtime.js +40 -9
  7. package/dist/cli/local-runtime.js.map +1 -1
  8. package/dist/core/local-eval/classify.d.ts +57 -0
  9. package/dist/core/local-eval/classify.js +121 -0
  10. package/dist/core/local-eval/classify.js.map +1 -0
  11. package/dist/core/local-eval/configuration.d.ts +37 -0
  12. package/dist/core/local-eval/configuration.js +171 -0
  13. package/dist/core/local-eval/configuration.js.map +1 -0
  14. package/dist/core/local-eval/index.d.ts +15 -0
  15. package/dist/core/local-eval/index.js +16 -0
  16. package/dist/core/local-eval/index.js.map +1 -0
  17. package/dist/core/local-eval/main.d.ts +31 -0
  18. package/dist/core/local-eval/main.js +174 -0
  19. package/dist/core/local-eval/main.js.map +1 -0
  20. package/dist/core/local-eval/report.d.ts +25 -0
  21. package/dist/core/local-eval/report.js +157 -0
  22. package/dist/core/local-eval/report.js.map +1 -0
  23. package/dist/core/local-eval/runner.d.ts +55 -0
  24. package/dist/core/local-eval/runner.js +290 -0
  25. package/dist/core/local-eval/runner.js.map +1 -0
  26. package/dist/core/local-eval/tasks.d.ts +22 -0
  27. package/dist/core/local-eval/tasks.js +303 -0
  28. package/dist/core/local-eval/tasks.js.map +1 -0
  29. package/dist/core/local-eval/trace.d.ts +67 -0
  30. package/dist/core/local-eval/trace.js +309 -0
  31. package/dist/core/local-eval/trace.js.map +1 -0
  32. package/dist/core/local-eval/types.d.ts +262 -0
  33. package/dist/core/local-eval/types.js +23 -0
  34. package/dist/core/local-eval/types.js.map +1 -0
  35. package/dist/core/local-runtime/llama/anthropic-proxy-host.d.ts +68 -0
  36. package/dist/core/local-runtime/llama/anthropic-proxy-host.js +120 -0
  37. package/dist/core/local-runtime/llama/anthropic-proxy-host.js.map +1 -0
  38. package/dist/core/local-runtime/llama/anthropic-proxy-process.d.ts +15 -0
  39. package/dist/core/local-runtime/llama/anthropic-proxy-process.js +21 -0
  40. package/dist/core/local-runtime/llama/anthropic-proxy-process.js.map +1 -0
  41. package/dist/core/local-runtime/llama/anthropic-proxy.d.ts +2 -1
  42. package/dist/core/local-runtime/llama/anthropic-proxy.js +14 -6
  43. package/dist/core/local-runtime/llama/anthropic-proxy.js.map +1 -1
  44. package/dist/core/local-runtime/llama/anthropic-shim.d.ts +33 -0
  45. package/dist/core/local-runtime/llama/anthropic-shim.js +59 -0
  46. package/dist/core/local-runtime/llama/anthropic-shim.js.map +1 -1
  47. package/dist/core/local-runtime/llama/config.d.ts +101 -6
  48. package/dist/core/local-runtime/llama/config.js +174 -6
  49. package/dist/core/local-runtime/llama/config.js.map +1 -1
  50. package/dist/core/local-runtime/llama/index.d.ts +15 -0
  51. package/dist/core/local-runtime/llama/index.js +13 -0
  52. package/dist/core/local-runtime/llama/index.js.map +1 -1
  53. package/dist/core/local-runtime/llama/paths.d.ts +17 -0
  54. package/dist/core/local-runtime/llama/paths.js +25 -0
  55. package/dist/core/local-runtime/llama/paths.js.map +1 -1
  56. package/dist/core/local-runtime/llama/proxy-invocation.d.ts +101 -0
  57. package/dist/core/local-runtime/llama/proxy-invocation.js +134 -0
  58. package/dist/core/local-runtime/llama/proxy-invocation.js.map +1 -0
  59. package/dist/core/local-runtime/llama/proxy-launchd.d.ts +83 -0
  60. package/dist/core/local-runtime/llama/proxy-launchd.js +216 -0
  61. package/dist/core/local-runtime/llama/proxy-launchd.js.map +1 -0
  62. package/dist/core/local-runtime/llama/shapes.d.ts +2 -1
  63. package/dist/core/local-runtime/llama/shapes.js +2 -1
  64. package/dist/core/local-runtime/llama/shapes.js.map +1 -1
  65. package/dist/core/verse/adapters/claude.js +16 -0
  66. package/dist/core/verse/adapters/claude.js.map +1 -1
  67. package/dist/core/verse/adapters/codex.js +60 -2
  68. package/dist/core/verse/adapters/codex.js.map +1 -1
  69. package/dist/core/verse/adapters/grok.js +13 -0
  70. package/dist/core/verse/adapters/grok.js.map +1 -1
  71. package/dist/core/verse/control-api.d.ts +8 -9
  72. package/dist/core/verse/control-api.js +14 -114
  73. package/dist/core/verse/control-api.js.map +1 -1
  74. package/dist/core/verse/github-api.d.ts +50 -0
  75. package/dist/core/verse/github-api.js +211 -0
  76. package/dist/core/verse/github-api.js.map +1 -0
  77. package/dist/core/verse/github-proposal.d.ts +64 -0
  78. package/dist/core/verse/github-proposal.js +186 -0
  79. package/dist/core/verse/github-proposal.js.map +1 -0
  80. package/dist/core/verse/github-repo.d.ts +104 -0
  81. package/dist/core/verse/github-repo.js +476 -0
  82. package/dist/core/verse/github-repo.js.map +1 -0
  83. package/dist/core/verse/github-types.d.ts +174 -0
  84. package/dist/core/verse/github-types.js +31 -0
  85. package/dist/core/verse/github-types.js.map +1 -0
  86. package/dist/core/verse/mcp-cli-health.d.ts +149 -0
  87. package/dist/core/verse/mcp-cli-health.js +227 -0
  88. package/dist/core/verse/mcp-cli-health.js.map +1 -0
  89. package/dist/core/verse/mcp-control-api.d.ts +68 -0
  90. package/dist/core/verse/mcp-control-api.js +322 -0
  91. package/dist/core/verse/mcp-control-api.js.map +1 -0
  92. package/dist/core/verse/mcp-mutation.d.ts +187 -0
  93. package/dist/core/verse/mcp-mutation.js +471 -0
  94. package/dist/core/verse/mcp-mutation.js.map +1 -0
  95. package/dist/core/verse/mcp-scope.d.ts +114 -0
  96. package/dist/core/verse/mcp-scope.js +149 -0
  97. package/dist/core/verse/mcp-scope.js.map +1 -0
  98. package/dist/core/verse/mcp-seat-view.d.ts +197 -0
  99. package/dist/core/verse/mcp-seat-view.js +322 -0
  100. package/dist/core/verse/mcp-seat-view.js.map +1 -0
  101. package/dist/core/verse/path-guard.d.ts +115 -0
  102. package/dist/core/verse/path-guard.js +171 -0
  103. package/dist/core/verse/path-guard.js.map +1 -0
  104. package/dist/core/verse/seats.js +30 -2
  105. package/dist/core/verse/seats.js.map +1 -1
  106. package/dist/core/verse/session-engine.js +60 -1
  107. package/dist/core/verse/session-engine.js.map +1 -1
  108. package/dist/core/verse/session-store.js +20 -0
  109. package/dist/core/verse/session-store.js.map +1 -1
  110. package/dist/core/verse/types.d.ts +225 -1
  111. package/dist/core/verse/types.js +26 -0
  112. package/dist/core/verse/types.js.map +1 -1
  113. package/dist/core/verse/verse-api.d.ts +16 -1
  114. package/dist/core/verse/verse-api.js +338 -16
  115. package/dist/core/verse/verse-api.js.map +1 -1
  116. package/dist/core/verse/workspaces.d.ts +130 -0
  117. package/dist/core/verse/workspaces.js +501 -0
  118. package/dist/core/verse/workspaces.js.map +1 -0
  119. package/dist/core/web/api.js +23 -0
  120. package/dist/core/web/api.js.map +1 -1
  121. package/dist/core/web/public/next/assets/{App-LuILXjWG.js → App-CsbPYQqy.js} +2 -2
  122. package/dist/core/web/public/next/assets/{App-LuILXjWG.js.map → App-CsbPYQqy.js.map} +1 -1
  123. package/dist/core/web/public/next/assets/{ApprovalsSection-B_Xor8JH.js → ApprovalsSection-CBZquyLK.js} +2 -2
  124. package/dist/core/web/public/next/assets/{ApprovalsSection-B_Xor8JH.js.map → ApprovalsSection-CBZquyLK.js.map} +1 -1
  125. package/dist/core/web/public/next/assets/AutonomySection-CQVpEdCE.js +2 -0
  126. package/dist/core/web/public/next/assets/{AutonomySection-BEwITtPJ.js.map → AutonomySection-CQVpEdCE.js.map} +1 -1
  127. package/dist/core/web/public/next/assets/AutonomySection-DTN6m6QE.css +1 -0
  128. package/dist/core/web/public/next/assets/ChatSection-DiRvNX3Q.css +1 -0
  129. package/dist/core/web/public/next/assets/ChatSection-DmE9K0Tg.js +77 -0
  130. package/dist/core/web/public/next/assets/ChatSection-DmE9K0Tg.js.map +1 -0
  131. package/dist/core/web/public/next/assets/{ConfirmDialog-BqkZdPs_.js → ConfirmDialog-deO0C0iY.js} +2 -2
  132. package/dist/core/web/public/next/assets/{ConfirmDialog-BqkZdPs_.js.map → ConfirmDialog-deO0C0iY.js.map} +1 -1
  133. package/dist/core/web/public/next/assets/{Dialog-CPmedg4S.js → Dialog-CNd7q89m.js} +2 -2
  134. package/dist/core/web/public/next/assets/{Dialog-CPmedg4S.js.map → Dialog-CNd7q89m.js.map} +1 -1
  135. package/dist/core/web/public/next/assets/{DiffViewer-DmPJbeM_.js → DiffViewer-DTO0KRuG.js} +2 -2
  136. package/dist/core/web/public/next/assets/{DiffViewer-DmPJbeM_.js.map → DiffViewer-DTO0KRuG.js.map} +1 -1
  137. package/dist/core/web/public/next/assets/{Epistemic-M3SxLHjq.js → Epistemic-53PikaUI.js} +2 -2
  138. package/dist/core/web/public/next/assets/{Epistemic-M3SxLHjq.js.map → Epistemic-53PikaUI.js.map} +1 -1
  139. package/dist/core/web/public/next/assets/{Meter-D_X7c9U3.js → Meter-DRWZQgr1.js} +2 -2
  140. package/dist/core/web/public/next/assets/{Meter-D_X7c9U3.js.map → Meter-DRWZQgr1.js.map} +1 -1
  141. package/dist/core/web/public/next/assets/{MutationTokenDialog-Ba5ZHu-8.js → MutationTokenDialog-DYF8n_RD.js} +2 -2
  142. package/dist/core/web/public/next/assets/{MutationTokenDialog-Ba5ZHu-8.js.map → MutationTokenDialog-DYF8n_RD.js.map} +1 -1
  143. package/dist/core/web/public/next/assets/{RefreshIndicator-vHGpmyzb.js → RefreshIndicator-DXJQXKyA.js} +2 -2
  144. package/dist/core/web/public/next/assets/{RefreshIndicator-vHGpmyzb.js.map → RefreshIndicator-DXJQXKyA.js.map} +1 -1
  145. package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-B5g6JEVk.js → ResourcePoolConsoleApp-hDSEFdLU.js} +2 -2
  146. package/dist/core/web/public/next/assets/{ResourcePoolConsoleApp-B5g6JEVk.js.map → ResourcePoolConsoleApp-hDSEFdLU.js.map} +1 -1
  147. package/dist/core/web/public/next/assets/{RouteErrorBoundary-CroxuKoi.js → RouteErrorBoundary-CZd7ln8v.js} +2 -2
  148. package/dist/core/web/public/next/assets/{RouteErrorBoundary-CroxuKoi.js.map → RouteErrorBoundary-CZd7ln8v.js.map} +1 -1
  149. package/dist/core/web/public/next/assets/{SettingsSection-p-7FCGfn.js → SettingsSection-C9Qs9JWW.js} +2 -2
  150. package/dist/core/web/public/next/assets/{SettingsSection-p-7FCGfn.js.map → SettingsSection-C9Qs9JWW.js.map} +1 -1
  151. package/dist/core/web/public/next/assets/{Skeleton-4qq4HuJO.js → Skeleton-YkcOpgFb.js} +2 -2
  152. package/dist/core/web/public/next/assets/{Skeleton-4qq4HuJO.js.map → Skeleton-YkcOpgFb.js.map} +1 -1
  153. package/dist/core/web/public/next/assets/{StatusBadge-BbEdqEGn.js → StatusBadge-DJw0LSPg.js} +2 -2
  154. package/dist/core/web/public/next/assets/{StatusBadge-BbEdqEGn.js.map → StatusBadge-DJw0LSPg.js.map} +1 -1
  155. package/dist/core/web/public/next/assets/{TableView-nyYs7Sv1.js → TableView-Bj-w1O6o.js} +2 -2
  156. package/dist/core/web/public/next/assets/{TableView-nyYs7Sv1.js.map → TableView-Bj-w1O6o.js.map} +1 -1
  157. package/dist/core/web/public/next/assets/{UniverseConsoleApp-DjTpQUrA.js → UniverseConsoleApp-Bs2fxaen.js} +2 -2
  158. package/dist/core/web/public/next/assets/{UniverseConsoleApp-DjTpQUrA.js.map → UniverseConsoleApp-Bs2fxaen.js.map} +1 -1
  159. package/dist/core/web/public/next/assets/{UniverseView-BtApwayT.js → UniverseView-B6b2hw-8.js} +2 -2
  160. package/dist/core/web/public/next/assets/{UniverseView-BtApwayT.js.map → UniverseView-B6b2hw-8.js.map} +1 -1
  161. package/dist/core/web/public/next/assets/{UsageSection-Db99N6ao.js → UsageSection-C__ziNkV.js} +2 -2
  162. package/dist/core/web/public/next/assets/{UsageSection-Db99N6ao.js.map → UsageSection-C__ziNkV.js.map} +1 -1
  163. package/dist/core/web/public/next/assets/{VerseConsoleApp-COa3J2dO.js → VerseConsoleApp-Cu5aG6J6.js} +3 -3
  164. package/dist/core/web/public/next/assets/VerseConsoleApp-Cu5aG6J6.js.map +1 -0
  165. package/dist/core/web/public/next/assets/{global-CNVapoV1.js → global-BHjd6fVZ.js} +2 -2
  166. package/dist/core/web/public/next/assets/{global-CNVapoV1.js.map → global-BHjd6fVZ.js.map} +1 -1
  167. package/dist/core/web/public/next/assets/index-CJ-e8YA9.js +2 -0
  168. package/dist/core/web/public/next/assets/index-CJ-e8YA9.js.map +1 -0
  169. package/dist/core/web/public/next/assets/{index-BkUlX2Se.js → index-CpgkRaTi.js} +3 -3
  170. package/dist/core/web/public/next/assets/{index-BkUlX2Se.js.map → index-CpgkRaTi.js.map} +1 -1
  171. package/dist/core/web/public/next/assets/{queries-C9XS4SYq.js → queries-856qUN3Y.js} +2 -2
  172. package/dist/core/web/public/next/assets/{queries-C9XS4SYq.js.map → queries-856qUN3Y.js.map} +1 -1
  173. package/dist/core/web/public/next/assets/{use-guarded-action-Bql1nA-z.js → use-guarded-action-DwaESKVy.js} +2 -2
  174. package/dist/core/web/public/next/assets/{use-guarded-action-Bql1nA-z.js.map → use-guarded-action-DwaESKVy.js.map} +1 -1
  175. package/dist/core/web/public/next/assets/verse-model-Dllo8rRt.js +2 -0
  176. package/dist/core/web/public/next/assets/{verse-model-IrNQKtER.js.map → verse-model-Dllo8rRt.js.map} +1 -1
  177. package/dist/core/web/public/next/assets/workspace-model-JgmWDKt4.js +2 -0
  178. package/dist/core/web/public/next/assets/workspace-model-JgmWDKt4.js.map +1 -0
  179. package/dist/core/web/public/next/index.html +1 -1
  180. package/dist/release-dependency-inventory.json +1 -1
  181. package/docs/README.md +4 -0
  182. package/package.json +1 -1
  183. package/dist/cli/activation.d.ts +0 -41
  184. package/dist/cli/activation.js +0 -485
  185. package/dist/cli/activation.js.map +0 -1
  186. package/dist/core/fleet/generated-repair-resolution.d.ts +0 -36
  187. package/dist/core/fleet/generated-repair-resolution.js +0 -245
  188. package/dist/core/fleet/generated-repair-resolution.js.map +0 -1
  189. package/dist/core/local-runtime/llama/port.d.ts +0 -17
  190. package/dist/core/local-runtime/llama/port.js +0 -47
  191. package/dist/core/local-runtime/llama/port.js.map +0 -1
  192. package/dist/core/local-runtime/llama/proxy-host.d.ts +0 -54
  193. package/dist/core/local-runtime/llama/proxy-host.js +0 -159
  194. package/dist/core/local-runtime/llama/proxy-host.js.map +0 -1
  195. package/dist/core/local-runtime/llama/proxy-process.d.ts +0 -148
  196. package/dist/core/local-runtime/llama/proxy-process.js +0 -250
  197. package/dist/core/local-runtime/llama/proxy-process.js.map +0 -1
  198. package/dist/core/local-runtime/llama/proxy-record.d.ts +0 -40
  199. package/dist/core/local-runtime/llama/proxy-record.js +0 -133
  200. package/dist/core/local-runtime/llama/proxy-record.js.map +0 -1
  201. package/dist/core/local-runtime/llama/proxy-supervisor.d.ts +0 -118
  202. package/dist/core/local-runtime/llama/proxy-supervisor.js +0 -511
  203. package/dist/core/local-runtime/llama/proxy-supervisor.js.map +0 -1
  204. package/dist/core/web/public/next/assets/AutonomySection-BB8RD-tv.css +0 -1
  205. package/dist/core/web/public/next/assets/AutonomySection-BEwITtPJ.js +0 -2
  206. package/dist/core/web/public/next/assets/ChatSection-DrxqhBPK.css +0 -1
  207. package/dist/core/web/public/next/assets/ChatSection-bRAzlMth.js +0 -77
  208. package/dist/core/web/public/next/assets/ChatSection-bRAzlMth.js.map +0 -1
  209. package/dist/core/web/public/next/assets/VerseConsoleApp-COa3J2dO.js.map +0 -1
  210. package/dist/core/web/public/next/assets/index-YNTgDG2b.js +0 -2
  211. package/dist/core/web/public/next/assets/index-YNTgDG2b.js.map +0 -1
  212. package/dist/core/web/public/next/assets/verse-model-IrNQKtER.js +0 -2
  213. package/node_modules/which/CHANGELOG.md +0 -166
@@ -0,0 +1,149 @@
1
+ /**
2
+ * core/verse/mcp-scope.ts — MCP scope, as reported by Locus.
3
+ *
4
+ * docs/VERSE-WORKSPACES.md §3: "a server appropriate for a personal account
5
+ * may be wrong for a client's. Locus already models exactly this — principal,
6
+ * tenant, sealed session — so scope should be asked of Locus rather than
7
+ * reinvented in the hub, and the hub should hold opaque references rather
8
+ * than copies of anything sensitive."
9
+ *
10
+ * So this module OWNS NO SCOPE. It asks `src/core/integrations/locus.ts` and
11
+ * projects the answer onto a transport shape. Two rules follow from that:
12
+ *
13
+ * 1. Every field here is an OPAQUE REFERENCE Locus already publishes in its
14
+ * own `agent report --json` (alias, tenant, binding_id, principal). None
15
+ * of them is a credential, and none of them is derived, inferred or
16
+ * cached by the hub. When Locus says nothing, this says nothing — it
17
+ * never substitutes a hub-local notion of "which account is this".
18
+ * 2. Mutation admission is NOT decided here either. It is decided by the
19
+ * repo's existing pre-mutate gate (`assertLocusPreMutate`), whose mode
20
+ * ladder (off | warn | enforce) is already the contract every other
21
+ * spawn site in this repo obeys — see src/core/run/engines.ts:662. A new
22
+ * surface inventing a stricter or looser rule than the dispatch path is
23
+ * exactly the "reinvented in the hub" failure this file exists to avoid.
24
+ *
25
+ * Pure projection + one shell-out through locus.ts. Never throws.
26
+ */
27
+ import { assertLocusPreMutate, locusAgentReport, } from '../integrations/locus.js';
28
+ // ---------------------------------------------------------------------------
29
+ // Projection
30
+ // ---------------------------------------------------------------------------
31
+ const UNAVAILABLE = {
32
+ available: false,
33
+ pinned: false,
34
+ aliasRef: null,
35
+ tenantRef: null,
36
+ principalRef: null,
37
+ bindingRef: null,
38
+ sealOk: null,
39
+ expired: null,
40
+ frozen: null,
41
+ expiresAt: null,
42
+ status: null,
43
+ statusOneline: null,
44
+ reason: 'mcp-scope-locus-unavailable',
45
+ };
46
+ /** A non-empty string, or null. Locus's optional fields arrive as either. */
47
+ function ref(value) {
48
+ return typeof value === 'string' && value.length > 0 ? value : null;
49
+ }
50
+ /** A boolean, or null when Locus did not report the flag at all. */
51
+ function flag(value) {
52
+ return typeof value === 'boolean' ? value : null;
53
+ }
54
+ /**
55
+ * Project one `locus agent report --json` probe onto {@link VerseMcpScope}.
56
+ *
57
+ * Pure: pass a canned {@link LocusProbeResult} in tests rather than shelling
58
+ * out. An unavailable probe, a null report and a null pin are three different
59
+ * facts and each gets its own reason.
60
+ */
61
+ export function projectVerseMcpScope(probe) {
62
+ if (!probe.available || probe.report === null)
63
+ return UNAVAILABLE;
64
+ const report = probe.report;
65
+ const pin = report.pin ?? null;
66
+ const base = {
67
+ available: true,
68
+ status: ref(report.status),
69
+ statusOneline: ref(report.status_oneline),
70
+ };
71
+ if (pin === null || pin.pinned !== true) {
72
+ return {
73
+ ...UNAVAILABLE,
74
+ ...base,
75
+ pinned: false,
76
+ reason: 'mcp-scope-unpinned',
77
+ };
78
+ }
79
+ const sealOk = flag(pin.seal_ok);
80
+ const expired = flag(pin.expired);
81
+ // Order matters: an expired pin whose seal never verified is reported as
82
+ // seal-unverified, because that is the more fundamental failure and the one
83
+ // the operator has to fix first.
84
+ const reason = sealOk === false ? 'mcp-scope-seal-unverified'
85
+ : expired === true ? 'mcp-scope-expired'
86
+ : 'mcp-scope-pinned';
87
+ return {
88
+ ...base,
89
+ pinned: true,
90
+ aliasRef: ref(pin.alias),
91
+ tenantRef: ref(pin.tenant),
92
+ principalRef: ref(pin.principal),
93
+ bindingRef: ref(pin.binding_id),
94
+ sealOk,
95
+ expired,
96
+ frozen: flag(pin.frozen),
97
+ expiresAt: ref(pin.expires_at),
98
+ reason,
99
+ };
100
+ }
101
+ /**
102
+ * Ask Locus for the current scope.
103
+ *
104
+ * `read` is the injection seam: production passes nothing and gets the real
105
+ * `locusAgentReport()` shell-out; tests pass a function returning a canned
106
+ * probe and never spawn anything. Never throws — locusAgentReport() already
107
+ * reports failure as `{available: false}` rather than raising.
108
+ */
109
+ export function readVerseMcpScope(read = locusAgentReport) {
110
+ try {
111
+ return projectVerseMcpScope(read());
112
+ }
113
+ catch {
114
+ return UNAVAILABLE;
115
+ }
116
+ }
117
+ /**
118
+ * The scope a write would land under, plus the repo's existing admission
119
+ * decision for it.
120
+ *
121
+ * `decide` and `read` are injection seams for tests. In production both shell
122
+ * out to Locus once each.
123
+ */
124
+ export function readVerseMcpScopeGate(options = {}) {
125
+ const scope = readVerseMcpScope(options.read ?? locusAgentReport);
126
+ let decision;
127
+ try {
128
+ decision = options.decide
129
+ ? options.decide()
130
+ // `arguments.length >= 2` inside assertLocusPreMutate distinguishes
131
+ // "config omitted -> read ~/.ashlr" from "config null -> no config", so
132
+ // the two calls must stay separate rather than passing `undefined`.
133
+ : 'config' in options
134
+ ? assertLocusPreMutate(options.env, options.config)
135
+ : assertLocusPreMutate(options.env);
136
+ }
137
+ catch {
138
+ // Fail closed on an unexpected throw: an undecidable gate is not an open
139
+ // one for a surface that installs arbitrary code.
140
+ decision = { allow: false, mode: 'enforce', blockers: ['locus pre-mutate gate could not be evaluated'] };
141
+ }
142
+ return {
143
+ scope,
144
+ mode: decision.mode,
145
+ allow: decision.allow,
146
+ blockers: [...decision.blockers],
147
+ };
148
+ }
149
+ //# sourceMappingURL=mcp-scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-scope.js","sourceRoot":"","sources":["../../../src/core/verse/mcp-scope.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,197 @@
1
+ /**
2
+ * core/verse/mcp-seat-view.ts — which MCP servers each Verse seat would load.
3
+ *
4
+ * READ-ONLY. Nothing in this file writes, installs, enables or disables
5
+ * anything. docs/VERSE-WORKSPACES.md §3 asks for exactly one thing first:
6
+ * "Surface which servers each seat would load, since a server configured for
7
+ * one account should not silently apply to another."
8
+ *
9
+ * ── The finding this module exists to make visible ──────────────────────────
10
+ *
11
+ * `discoverMcpServers()` scans HOME-level JSON configs (see
12
+ * `knownConfigPaths()`): ~/.claude.json, ~/.claude/settings.json, ~/.mcp.json,
13
+ * ~/.ashlrcode/settings.json, ~/.ashlr-workbench/settings.json,
14
+ * ~/.aw/settings.json, ~/.ashlr/settings.json.
15
+ *
16
+ * NO VERSE SEAT READS ANY OF THEM. Verified against the adapters and the
17
+ * launcher, not recalled:
18
+ *
19
+ * - CLAUDE and LOCAL seats: `buildClaudeLaunch` (src/core/verse/adapters/
20
+ * claude.ts) puts `--strict-mcp-config --mcp-config '{"mcpServers":{}}'`
21
+ * in every turn's argv, for both engines. That is a complete override:
22
+ * the seat loads ZERO MCP servers, whatever any config file says.
23
+ * - CODEX and GROK seats: their adapters add no MCP flag, so each loads its
24
+ * OWN native config — and the account is pinned by a private per-account
25
+ * state directory (`CODEX_HOME` / `GROK_HOME` / `CLAUDE_CONFIG_DIR` =
26
+ * `<profile>/native-state`, src/core/resources/native-profile.ts:72-78,
27
+ * :108). That directory is per ACCOUNT, so two Codex accounts already
28
+ * cannot share a server by accident. Their config is `config.toml` —
29
+ * TOML, which `discoverMcpServers()` (JSON only) cannot read.
30
+ *
31
+ * Reporting a machine-wide server list as "what this seat runs" would
32
+ * therefore be a lie in every direction at once: it would show servers no
33
+ * seat loads, and hide the per-account TOML that Codex and Grok actually do.
34
+ * So this module reports the two things separately and names the reason for
35
+ * each, and reports EMPTY rather than guessing.
36
+ *
37
+ * ── What may leave this module ──────────────────────────────────────────────
38
+ *
39
+ * Never: a launcher argv, an absolute native-profile path, a home-anchored
40
+ * absolute path, or a real env value. Config files are identified by their
41
+ * path RELATIVE TO $HOME (`.claude.json`, `.ashlr/settings.json`), which
42
+ * names the file precisely and carries no username. Every spec crosses the
43
+ * boundary through `redactEnv()`, whose whole reason for existing is that
44
+ * these specs carry secrets.
45
+ */
46
+ import type { McpServerSpec } from '../types.js';
47
+ import type { VerseEngine } from './types.js';
48
+ import { type VerseAccountIdentity } from './accounts.js';
49
+ import { type VerseMcpScope } from './mcp-scope.js';
50
+ /**
51
+ * Engines whose adapter argv pins an EMPTY `--mcp-config` under
52
+ * `--strict-mcp-config`, so the seat can load nothing at all.
53
+ *
54
+ * Both entries come from one function: `buildClaudeLaunch` serves engine
55
+ * 'claude' and engine 'local' alike (the latter is the same `claude` binary
56
+ * with ANTHROPIC_BASE_URL pointed at Ollama).
57
+ *
58
+ * If that argv ever loses those two flags this constant becomes a lie, which
59
+ * is why `test/verse-mcp-seat-view.test.ts` asserts the flags are still in the
60
+ * adapter's real output rather than trusting this list.
61
+ */
62
+ export declare const VERSE_MCP_ISOLATED_ENGINES: readonly VerseEngine[];
63
+ /** The two argv flags that make an isolated seat isolated. Asserted in tests. */
64
+ export declare const VERSE_MCP_ISOLATION_FLAGS: readonly string[];
65
+ /** Why a seat loads what it loads. Machine-readable, verbatim, never prose. */
66
+ export type VerseMcpSeatReason =
67
+ /** The adapter pins an empty --mcp-config: this seat loads nothing. */
68
+ 'mcp-seat-isolated-by-adapter'
69
+ /** The account's private config file does not exist. */
70
+ | 'mcp-account-config-absent'
71
+ /** The config exists but is TOML, which this JSON-only discovery does not parse. */
72
+ | 'mcp-account-config-not-json'
73
+ /** The config exists, is JSON, and was read. */
74
+ | 'mcp-account-config-read'
75
+ /** The config exists but could not be read or parsed. */
76
+ | 'mcp-account-config-unreadable'
77
+ /** The account's private state directory could not be resolved. */
78
+ | 'mcp-account-profile-unresolved';
79
+ /**
80
+ * One MCP server, safe to serialize.
81
+ *
82
+ * `command` and `args` are shown IN FULL and verbatim: docs/VERSE-WORKSPACES.md
83
+ * §3 requires the operator to read what they are about to run. `env` keys are
84
+ * shown; every env VALUE is the literal `'<set>'` from `redactEnv`.
85
+ */
86
+ export interface VerseMcpServerView {
87
+ name: string;
88
+ command: string;
89
+ args: string[];
90
+ /** Keys only — every value is `'<set>'`. Null when the spec sets no env. */
91
+ env: Record<string, string> | null;
92
+ /** Config file path RELATIVE TO $HOME. Never absolute, never a username. */
93
+ sourceRef: string;
94
+ }
95
+ /** What one seat would load, and why. */
96
+ export interface VerseMcpSeatView {
97
+ seatId: string;
98
+ label: string;
99
+ engine: VerseEngine;
100
+ accountId: string;
101
+ /** Servers this seat would actually load. Empty is a real answer. */
102
+ servers: VerseMcpServerView[];
103
+ reason: VerseMcpSeatReason;
104
+ /**
105
+ * The account config file this seat reads, named relative to the account's
106
+ * own private state directory (e.g. `config.toml`). The directory itself is
107
+ * private and is never published.
108
+ */
109
+ configFile: string | null;
110
+ configFormat: 'json' | 'toml' | null;
111
+ /** Plain facts the UI must show rather than implying a fault. */
112
+ notes: string[];
113
+ }
114
+ /** Servers configured on this machine that no Verse seat reads. */
115
+ export interface VerseMcpMachineRegistry {
116
+ servers: VerseMcpServerView[];
117
+ /**
118
+ * TRUE when at least one server is configured machine-wide. Combined with
119
+ * every seat reporting none, that is the fact worth surfacing loudly.
120
+ */
121
+ configured: boolean;
122
+ note: string;
123
+ }
124
+ export interface VerseMcpSnapshot {
125
+ sampledAt: string;
126
+ seats: VerseMcpSeatView[];
127
+ machine: VerseMcpMachineRegistry;
128
+ scope: VerseMcpScope;
129
+ notes: string[];
130
+ }
131
+ export declare const VERSE_MCP_ISOLATED_NOTE: string;
132
+ export declare const VERSE_MCP_TOML_NOTE: string;
133
+ export declare const VERSE_MCP_PER_ACCOUNT_NOTE: string;
134
+ export declare const VERSE_MCP_MACHINE_UNUSED_NOTE: string;
135
+ export declare const VERSE_MCP_MACHINE_EMPTY_NOTE = "No MCP servers are configured in this machine's home-level configs.";
136
+ /**
137
+ * Name a config file by its path relative to $HOME (`.claude.json`,
138
+ * `.ashlr/settings.json`). Precise enough to act on, and free of the username
139
+ * that an absolute path would carry.
140
+ *
141
+ * A path outside $HOME is reported as `(outside home)` — its basename alone
142
+ * would be ambiguous and its full path is not ours to publish.
143
+ */
144
+ export declare function homeRelativeSourceRef(path: string, home?: string): string;
145
+ /** Project one discovered spec for transport, env values redacted. */
146
+ export declare function projectMcpServerView(spec: McpServerSpec, home?: string): VerseMcpServerView;
147
+ /**
148
+ * Resolve an account's private native state directory WITHOUT publishing it.
149
+ *
150
+ * connections.json carries each account's launcher argv. That argv is the
151
+ * account's identity and never leaves the server (src/core/verse/seats.ts:9).
152
+ * We read it here only to locate `<profile>/profile.json`, whose
153
+ * `nativeStatePath` is the directory the provider CLI is pinned to
154
+ * (`CODEX_HOME` / `GROK_HOME` / `CLAUDE_CONFIG_DIR`).
155
+ *
156
+ * Returns null rather than guessing when anything is missing.
157
+ */
158
+ export declare function resolveAccountStateRoots(accountsRoot: string): Map<string, string>;
159
+ /** The minimum a seat must carry for this view. Keeps tests free of fixtures. */
160
+ export interface VerseMcpSeatInput {
161
+ id: string;
162
+ label: string;
163
+ engine: VerseEngine;
164
+ accountId: string;
165
+ }
166
+ /**
167
+ * Derive what one seat would load.
168
+ *
169
+ * `stateRoots` maps accountId -> private state directory (from
170
+ * {@link resolveAccountStateRoots}). A seat whose account is absent from it
171
+ * reports `mcp-account-profile-unresolved` rather than falling back to a
172
+ * machine-wide list.
173
+ */
174
+ export declare function deriveVerseMcpSeatView(seat: VerseMcpSeatInput, stateRoots: ReadonlyMap<string, string>, home?: string): VerseMcpSeatView;
175
+ /** The machine-wide registry, and the fact that no seat reads it. */
176
+ export declare function buildVerseMcpMachineRegistry(paths?: string[], home?: string): VerseMcpMachineRegistry;
177
+ export interface VerseMcpSnapshotOptions {
178
+ accountsRoot: string;
179
+ /** Seats to describe. Callers pass `discoverSeats()`'s public seat list. */
180
+ seats: readonly VerseMcpSeatInput[];
181
+ /** Injected machine config paths (tests). Defaults to knownConfigPaths(). */
182
+ machineConfigPaths?: string[];
183
+ /** Injected scope read (tests). Defaults to the real Locus shell-out. */
184
+ scope?: VerseMcpScope;
185
+ /** Injected home (tests). */
186
+ home?: string;
187
+ }
188
+ /**
189
+ * The read-only answer to "which MCP servers would each seat load".
190
+ *
191
+ * Deliberately does no discovery of its own beyond the two registries: it
192
+ * never enables, disables or reorders anything, and it never reports a server
193
+ * against a seat that would not load it.
194
+ */
195
+ export declare function buildVerseMcpSnapshot(options: VerseMcpSnapshotOptions): VerseMcpSnapshot;
196
+ /** Account identities, re-exported so a caller needs one import for the view. */
197
+ export declare function verseMcpAccountIdentities(accountsRoot: string): VerseAccountIdentity[];
@@ -0,0 +1,322 @@
1
+ /**
2
+ * core/verse/mcp-seat-view.ts — which MCP servers each Verse seat would load.
3
+ *
4
+ * READ-ONLY. Nothing in this file writes, installs, enables or disables
5
+ * anything. docs/VERSE-WORKSPACES.md §3 asks for exactly one thing first:
6
+ * "Surface which servers each seat would load, since a server configured for
7
+ * one account should not silently apply to another."
8
+ *
9
+ * ── The finding this module exists to make visible ──────────────────────────
10
+ *
11
+ * `discoverMcpServers()` scans HOME-level JSON configs (see
12
+ * `knownConfigPaths()`): ~/.claude.json, ~/.claude/settings.json, ~/.mcp.json,
13
+ * ~/.ashlrcode/settings.json, ~/.ashlr-workbench/settings.json,
14
+ * ~/.aw/settings.json, ~/.ashlr/settings.json.
15
+ *
16
+ * NO VERSE SEAT READS ANY OF THEM. Verified against the adapters and the
17
+ * launcher, not recalled:
18
+ *
19
+ * - CLAUDE and LOCAL seats: `buildClaudeLaunch` (src/core/verse/adapters/
20
+ * claude.ts) puts `--strict-mcp-config --mcp-config '{"mcpServers":{}}'`
21
+ * in every turn's argv, for both engines. That is a complete override:
22
+ * the seat loads ZERO MCP servers, whatever any config file says.
23
+ * - CODEX and GROK seats: their adapters add no MCP flag, so each loads its
24
+ * OWN native config — and the account is pinned by a private per-account
25
+ * state directory (`CODEX_HOME` / `GROK_HOME` / `CLAUDE_CONFIG_DIR` =
26
+ * `<profile>/native-state`, src/core/resources/native-profile.ts:72-78,
27
+ * :108). That directory is per ACCOUNT, so two Codex accounts already
28
+ * cannot share a server by accident. Their config is `config.toml` —
29
+ * TOML, which `discoverMcpServers()` (JSON only) cannot read.
30
+ *
31
+ * Reporting a machine-wide server list as "what this seat runs" would
32
+ * therefore be a lie in every direction at once: it would show servers no
33
+ * seat loads, and hide the per-account TOML that Codex and Grok actually do.
34
+ * So this module reports the two things separately and names the reason for
35
+ * each, and reports EMPTY rather than guessing.
36
+ *
37
+ * ── What may leave this module ──────────────────────────────────────────────
38
+ *
39
+ * Never: a launcher argv, an absolute native-profile path, a home-anchored
40
+ * absolute path, or a real env value. Config files are identified by their
41
+ * path RELATIVE TO $HOME (`.claude.json`, `.ashlr/settings.json`), which
42
+ * names the file precisely and carries no username. Every spec crosses the
43
+ * boundary through `redactEnv()`, whose whole reason for existing is that
44
+ * these specs carry secrets.
45
+ */
46
+ import { existsSync, readFileSync } from 'node:fs';
47
+ import { homedir } from 'node:os';
48
+ import { dirname, join, relative, isAbsolute, sep } from 'node:path';
49
+ import { discoverMcpServers, redactEnv } from '../mcp-registry.js';
50
+ import { readVerseAccountIdentities } from './accounts.js';
51
+ import { readVerseMcpScope } from './mcp-scope.js';
52
+ // ---------------------------------------------------------------------------
53
+ // Verified facts about the adapters, encoded once
54
+ // ---------------------------------------------------------------------------
55
+ /**
56
+ * Engines whose adapter argv pins an EMPTY `--mcp-config` under
57
+ * `--strict-mcp-config`, so the seat can load nothing at all.
58
+ *
59
+ * Both entries come from one function: `buildClaudeLaunch` serves engine
60
+ * 'claude' and engine 'local' alike (the latter is the same `claude` binary
61
+ * with ANTHROPIC_BASE_URL pointed at Ollama).
62
+ *
63
+ * If that argv ever loses those two flags this constant becomes a lie, which
64
+ * is why `test/verse-mcp-seat-view.test.ts` asserts the flags are still in the
65
+ * adapter's real output rather than trusting this list.
66
+ */
67
+ export const VERSE_MCP_ISOLATED_ENGINES = ['claude', 'local'];
68
+ /** The two argv flags that make an isolated seat isolated. Asserted in tests. */
69
+ export const VERSE_MCP_ISOLATION_FLAGS = ['--strict-mcp-config', '--mcp-config'];
70
+ /**
71
+ * Per-account native config file, relative to that account's private state
72
+ * directory. Claude's is listed for completeness even though a Claude seat is
73
+ * isolated by its adapter before the file is ever consulted.
74
+ */
75
+ const NATIVE_CONFIG_FILE = {
76
+ codex: { file: 'config.toml', format: 'toml' },
77
+ grok: { file: 'config.toml', format: 'toml' },
78
+ claude: { file: '.claude.json', format: 'json' },
79
+ };
80
+ // ---------------------------------------------------------------------------
81
+ // Notes (plain language, no machine codes)
82
+ // ---------------------------------------------------------------------------
83
+ export const VERSE_MCP_ISOLATED_NOTE = 'Every turn on this seat is launched with --strict-mcp-config and an empty --mcp-config, ' +
84
+ 'so it loads no MCP servers at all regardless of what any config file holds.';
85
+ export const VERSE_MCP_TOML_NOTE = 'This account keeps its MCP servers in a TOML config, which is read by the provider CLI ' +
86
+ 'itself and is not parsed here. Hub cannot list or change it without rewriting the ' +
87
+ "operator's TOML, which it deliberately does not do.";
88
+ export const VERSE_MCP_PER_ACCOUNT_NOTE = 'This account runs against its own private state directory, so a server configured for ' +
89
+ 'another account does not reach it.';
90
+ export const VERSE_MCP_MACHINE_UNUSED_NOTE = 'These servers are configured in this machine\'s home-level configs. No Verse seat reads ' +
91
+ 'them: Claude and local seats are launched with an empty --mcp-config, and Codex and Grok ' +
92
+ 'seats read their own per-account config instead.';
93
+ export const VERSE_MCP_MACHINE_EMPTY_NOTE = 'No MCP servers are configured in this machine\'s home-level configs.';
94
+ // ---------------------------------------------------------------------------
95
+ // Path handling — nothing absolute crosses the boundary
96
+ // ---------------------------------------------------------------------------
97
+ /**
98
+ * Name a config file by its path relative to $HOME (`.claude.json`,
99
+ * `.ashlr/settings.json`). Precise enough to act on, and free of the username
100
+ * that an absolute path would carry.
101
+ *
102
+ * A path outside $HOME is reported as `(outside home)` — its basename alone
103
+ * would be ambiguous and its full path is not ours to publish.
104
+ */
105
+ export function homeRelativeSourceRef(path, home = homedir()) {
106
+ if (!isAbsolute(path))
107
+ return path;
108
+ const rel = relative(home, path);
109
+ if (rel.length === 0 || rel.startsWith('..') || isAbsolute(rel))
110
+ return '(outside home)';
111
+ return rel.split(sep).join('/');
112
+ }
113
+ /** Project one discovered spec for transport, env values redacted. */
114
+ export function projectMcpServerView(spec, home = homedir()) {
115
+ const safe = redactEnv(spec);
116
+ return {
117
+ name: safe.name,
118
+ command: safe.command,
119
+ args: [...safe.args],
120
+ env: safe.env && Object.keys(safe.env).length > 0 ? { ...safe.env } : null,
121
+ sourceRef: homeRelativeSourceRef(safe.source, home),
122
+ };
123
+ }
124
+ // ---------------------------------------------------------------------------
125
+ // Per-account private state directory
126
+ // ---------------------------------------------------------------------------
127
+ /**
128
+ * Resolve an account's private native state directory WITHOUT publishing it.
129
+ *
130
+ * connections.json carries each account's launcher argv. That argv is the
131
+ * account's identity and never leaves the server (src/core/verse/seats.ts:9).
132
+ * We read it here only to locate `<profile>/profile.json`, whose
133
+ * `nativeStatePath` is the directory the provider CLI is pinned to
134
+ * (`CODEX_HOME` / `GROK_HOME` / `CLAUDE_CONFIG_DIR`).
135
+ *
136
+ * Returns null rather than guessing when anything is missing.
137
+ */
138
+ export function resolveAccountStateRoots(accountsRoot) {
139
+ const out = new Map();
140
+ let parsed;
141
+ try {
142
+ const raw = readFileSync(join(accountsRoot, 'connections.json'), 'utf8');
143
+ if (raw.length > 1024 * 1024)
144
+ return out;
145
+ parsed = JSON.parse(raw);
146
+ }
147
+ catch {
148
+ return out;
149
+ }
150
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
151
+ return out;
152
+ const accounts = parsed['accounts'];
153
+ if (!Array.isArray(accounts))
154
+ return out;
155
+ for (const entry of accounts) {
156
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
157
+ continue;
158
+ const row = entry;
159
+ const id = row['id'];
160
+ const command = row['command'];
161
+ if (typeof id !== 'string' || id.length === 0 || !Array.isArray(command))
162
+ continue;
163
+ const launcher = command.find((part) => typeof part === 'string' && part.endsWith('launcher.mjs'));
164
+ if (launcher === undefined)
165
+ continue;
166
+ // profile.json is the launcher's own manifest (ResourceNativeProfile). It
167
+ // also carries the argv, so only `nativeStatePath` is lifted out of it.
168
+ let stateRoot = null;
169
+ try {
170
+ const manifest = JSON.parse(readFileSync(join(dirname(launcher), 'profile.json'), 'utf8'));
171
+ if (manifest !== null && typeof manifest === 'object' && !Array.isArray(manifest)) {
172
+ const value = manifest['nativeStatePath'];
173
+ if (typeof value === 'string' && value.length > 0)
174
+ stateRoot = value;
175
+ }
176
+ }
177
+ catch {
178
+ stateRoot = null;
179
+ }
180
+ if (stateRoot !== null)
181
+ out.set(id, stateRoot);
182
+ }
183
+ return out;
184
+ }
185
+ /**
186
+ * Read one account's private JSON MCP registry.
187
+ *
188
+ * Only ever called for a JSON-format config. TOML is refused upstream rather
189
+ * than half-parsed.
190
+ */
191
+ function readAccountJsonServers(stateRoot, file, home) {
192
+ const path = join(stateRoot, file);
193
+ if (!existsSync(path))
194
+ return { servers: [], ok: true };
195
+ // discoverMcpServers takes injected paths and never throws; a malformed
196
+ // config comes back as an empty registry, which we cannot distinguish from
197
+ // a config with no servers. That ambiguity is acceptable here because both
198
+ // render identically ("this seat loads nothing from its own config").
199
+ try {
200
+ const registry = discoverMcpServers([path]);
201
+ return { servers: registry.servers.map((spec) => projectMcpServerView(spec, home)), ok: true };
202
+ }
203
+ catch {
204
+ return { servers: [], ok: false };
205
+ }
206
+ }
207
+ /**
208
+ * Derive what one seat would load.
209
+ *
210
+ * `stateRoots` maps accountId -> private state directory (from
211
+ * {@link resolveAccountStateRoots}). A seat whose account is absent from it
212
+ * reports `mcp-account-profile-unresolved` rather than falling back to a
213
+ * machine-wide list.
214
+ */
215
+ export function deriveVerseMcpSeatView(seat, stateRoots, home = homedir()) {
216
+ const base = { seatId: seat.id, label: seat.label, engine: seat.engine, accountId: seat.accountId };
217
+ // 1. Adapter isolation wins over every config on disk. Checking it FIRST is
218
+ // the point: a Claude seat with a fully populated ~/.claude.json still
219
+ // loads nothing, and saying otherwise is the exact lie this file avoids.
220
+ if (VERSE_MCP_ISOLATED_ENGINES.includes(seat.engine)) {
221
+ return {
222
+ ...base,
223
+ servers: [],
224
+ reason: 'mcp-seat-isolated-by-adapter',
225
+ configFile: null,
226
+ configFormat: null,
227
+ notes: [VERSE_MCP_ISOLATED_NOTE],
228
+ };
229
+ }
230
+ // 2. Native seats read their own per-account config.
231
+ const provider = seat.engine;
232
+ const config = NATIVE_CONFIG_FILE[provider];
233
+ if (config === undefined) {
234
+ return {
235
+ ...base,
236
+ servers: [],
237
+ reason: 'mcp-account-profile-unresolved',
238
+ configFile: null,
239
+ configFormat: null,
240
+ notes: [],
241
+ };
242
+ }
243
+ const stateRoot = stateRoots.get(seat.accountId);
244
+ if (stateRoot === undefined) {
245
+ return {
246
+ ...base,
247
+ servers: [],
248
+ reason: 'mcp-account-profile-unresolved',
249
+ configFile: config.file,
250
+ configFormat: config.format,
251
+ notes: [VERSE_MCP_PER_ACCOUNT_NOTE],
252
+ };
253
+ }
254
+ const exists = existsSync(join(stateRoot, config.file));
255
+ if (!exists) {
256
+ return {
257
+ ...base,
258
+ servers: [],
259
+ reason: 'mcp-account-config-absent',
260
+ configFile: config.file,
261
+ configFormat: config.format,
262
+ notes: [VERSE_MCP_PER_ACCOUNT_NOTE],
263
+ };
264
+ }
265
+ if (config.format === 'toml') {
266
+ return {
267
+ ...base,
268
+ servers: [],
269
+ reason: 'mcp-account-config-not-json',
270
+ configFile: config.file,
271
+ configFormat: 'toml',
272
+ notes: [VERSE_MCP_TOML_NOTE, VERSE_MCP_PER_ACCOUNT_NOTE],
273
+ };
274
+ }
275
+ const read = readAccountJsonServers(stateRoot, config.file, home);
276
+ return {
277
+ ...base,
278
+ servers: read.servers,
279
+ reason: read.ok ? 'mcp-account-config-read' : 'mcp-account-config-unreadable',
280
+ configFile: config.file,
281
+ configFormat: 'json',
282
+ notes: [VERSE_MCP_PER_ACCOUNT_NOTE],
283
+ };
284
+ }
285
+ // ---------------------------------------------------------------------------
286
+ // Snapshot
287
+ // ---------------------------------------------------------------------------
288
+ /** The machine-wide registry, and the fact that no seat reads it. */
289
+ export function buildVerseMcpMachineRegistry(paths, home = homedir()) {
290
+ const registry = discoverMcpServers(paths);
291
+ const servers = registry.servers.map((spec) => projectMcpServerView(spec, home));
292
+ return {
293
+ servers,
294
+ configured: servers.length > 0,
295
+ note: servers.length > 0 ? VERSE_MCP_MACHINE_UNUSED_NOTE : VERSE_MCP_MACHINE_EMPTY_NOTE,
296
+ };
297
+ }
298
+ /**
299
+ * The read-only answer to "which MCP servers would each seat load".
300
+ *
301
+ * Deliberately does no discovery of its own beyond the two registries: it
302
+ * never enables, disables or reorders anything, and it never reports a server
303
+ * against a seat that would not load it.
304
+ */
305
+ export function buildVerseMcpSnapshot(options) {
306
+ const home = options.home ?? homedir();
307
+ const stateRoots = resolveAccountStateRoots(options.accountsRoot);
308
+ const seats = options.seats.map((seat) => deriveVerseMcpSeatView(seat, stateRoots, home));
309
+ const machine = buildVerseMcpMachineRegistry(options.machineConfigPaths, home);
310
+ const scope = options.scope ?? readVerseMcpScope();
311
+ const notes = [];
312
+ if (machine.configured && seats.every((seat) => seat.servers.length === 0)) {
313
+ notes.push(`${machine.servers.length} MCP server${machine.servers.length === 1 ? ' is' : 's are'} configured ` +
314
+ 'on this machine and no Verse seat loads any of them.');
315
+ }
316
+ return { sampledAt: new Date().toISOString(), seats, machine, scope, notes };
317
+ }
318
+ /** Account identities, re-exported so a caller needs one import for the view. */
319
+ export function verseMcpAccountIdentities(accountsRoot) {
320
+ return readVerseAccountIdentities(accountsRoot);
321
+ }
322
+ //# sourceMappingURL=mcp-seat-view.js.map