@cybedefend/vibedefend 1.5.0 → 1.7.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 (264) hide show
  1. package/README.md +71 -2
  2. package/dist/api/gateway-request.js +49 -0
  3. package/dist/api/gateway-request.js.map +1 -0
  4. package/dist/api/projects.js +67 -0
  5. package/dist/api/projects.js.map +1 -1
  6. package/dist/api/repository-link.js +157 -0
  7. package/dist/api/repository-link.js.map +1 -0
  8. package/dist/auth/auth-store.js +107 -25
  9. package/dist/auth/auth-store.js.map +1 -1
  10. package/dist/auth/auth.js +21 -0
  11. package/dist/auth/auth.js.map +1 -1
  12. package/dist/clients/cursor.js +5 -1
  13. package/dist/clients/cursor.js.map +1 -1
  14. package/dist/clients/vscode.js +38 -29
  15. package/dist/clients/vscode.js.map +1 -1
  16. package/dist/commands/mcp/allow.js +219 -0
  17. package/dist/commands/mcp/allow.js.map +1 -0
  18. package/dist/commands/mcp/connect.js +337 -0
  19. package/dist/commands/mcp/connect.js.map +1 -0
  20. package/dist/commands/mcp/index.js +170 -0
  21. package/dist/commands/mcp/index.js.map +1 -0
  22. package/dist/commands/mcp/proxy.js +320 -0
  23. package/dist/commands/mcp/proxy.js.map +1 -0
  24. package/dist/commands/mcp/scan.js +349 -0
  25. package/dist/commands/mcp/scan.js.map +1 -0
  26. package/dist/commands/mcp/sync.js +215 -0
  27. package/dist/commands/mcp/sync.js.map +1 -0
  28. package/dist/config.js +6 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/diagnostics.js +78 -12
  31. package/dist/diagnostics.js.map +1 -1
  32. package/dist/doctor.js +19 -0
  33. package/dist/doctor.js.map +1 -1
  34. package/dist/guards-evaluator/analysis-types.js +27 -0
  35. package/dist/guards-evaluator/analysis-types.js.map +1 -0
  36. package/dist/guards-evaluator/command-analysis.js +572 -0
  37. package/dist/guards-evaluator/command-analysis.js.map +1 -0
  38. package/dist/guards-evaluator/command-view.js +586 -0
  39. package/dist/guards-evaluator/command-view.js.map +1 -0
  40. package/dist/guards-evaluator/env.js +11 -0
  41. package/dist/guards-evaluator/env.js.map +1 -0
  42. package/dist/guards-evaluator/evaluate.js +91 -41
  43. package/dist/guards-evaluator/evaluate.js.map +1 -1
  44. package/dist/guards-evaluator/index.js +2 -1
  45. package/dist/guards-evaluator/index.js.map +1 -1
  46. package/dist/guards-evaluator/inline-code.js +259 -0
  47. package/dist/guards-evaluator/inline-code.js.map +1 -0
  48. package/dist/guards-evaluator/normalize.js +716 -0
  49. package/dist/guards-evaluator/normalize.js.map +1 -0
  50. package/dist/guards-evaluator/preprocess.js +13 -5
  51. package/dist/guards-evaluator/preprocess.js.map +1 -1
  52. package/dist/guards-evaluator/redact.js +19 -0
  53. package/dist/guards-evaluator/redact.js.map +1 -1
  54. package/dist/guards-evaluator/regex.js +25 -3
  55. package/dist/guards-evaluator/regex.js.map +1 -1
  56. package/dist/guards-evaluator/safe-commands.js +155 -0
  57. package/dist/guards-evaluator/safe-commands.js.map +1 -0
  58. package/dist/guards-evaluator/triage.js +315 -0
  59. package/dist/guards-evaluator/triage.js.map +1 -0
  60. package/dist/hook-runner.js +24679 -5079
  61. package/dist/hooks/install.js +38 -0
  62. package/dist/hooks/install.js.map +1 -1
  63. package/dist/hooks/runtime/config.js +38 -0
  64. package/dist/hooks/runtime/config.js.map +1 -1
  65. package/dist/hooks/runtime/emit.js +94 -61
  66. package/dist/hooks/runtime/emit.js.map +1 -1
  67. package/dist/hooks/runtime/fetch-rules.js +22 -7
  68. package/dist/hooks/runtime/fetch-rules.js.map +1 -1
  69. package/dist/hooks/runtime/gateway-http-error.js +34 -0
  70. package/dist/hooks/runtime/gateway-http-error.js.map +1 -0
  71. package/dist/hooks/runtime/guard-rules-cache.js +139 -17
  72. package/dist/hooks/runtime/guard-rules-cache.js.map +1 -1
  73. package/dist/hooks/runtime/guard-violations-buffer.js +321 -48
  74. package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -1
  75. package/dist/hooks/runtime/judge-cache.js +6 -0
  76. package/dist/hooks/runtime/judge-cache.js.map +1 -1
  77. package/dist/hooks/runtime/judge-triage.js +193 -0
  78. package/dist/hooks/runtime/judge-triage.js.map +1 -0
  79. package/dist/hooks/runtime/judge.js +7 -1
  80. package/dist/hooks/runtime/judge.js.map +1 -1
  81. package/dist/hooks/runtime/pre-compact.js +12 -6
  82. package/dist/hooks/runtime/pre-compact.js.map +1 -1
  83. package/dist/hooks/runtime/resolve.js +90 -8
  84. package/dist/hooks/runtime/resolve.js.map +1 -1
  85. package/dist/hooks/runtime/retry-policy.js +57 -0
  86. package/dist/hooks/runtime/retry-policy.js.map +1 -0
  87. package/dist/hooks/runtime/review-sessions.js +74 -0
  88. package/dist/hooks/runtime/review-sessions.js.map +1 -0
  89. package/dist/hooks/runtime/script-resolve.js +695 -0
  90. package/dist/hooks/runtime/script-resolve.js.map +1 -0
  91. package/dist/hooks/runtime/session-activation.js +5 -1
  92. package/dist/hooks/runtime/session-activation.js.map +1 -1
  93. package/dist/hooks/runtime/session-review.js +163 -45
  94. package/dist/hooks/runtime/session-review.js.map +1 -1
  95. package/dist/hooks/runtime/session-start.js +2 -1
  96. package/dist/hooks/runtime/session-start.js.map +1 -1
  97. package/dist/hooks/runtime/session-writes.js +232 -0
  98. package/dist/hooks/runtime/session-writes.js.map +1 -0
  99. package/dist/hooks/runtime/sniff.js +26 -7
  100. package/dist/hooks/runtime/sniff.js.map +1 -1
  101. package/dist/hooks/runtime/target-resolve.js +336 -0
  102. package/dist/hooks/runtime/target-resolve.js.map +1 -0
  103. package/dist/hooks/runtime/unmapped-tools.js +93 -0
  104. package/dist/hooks/runtime/unmapped-tools.js.map +1 -0
  105. package/dist/hooks/runtime/usage-block.js +4 -0
  106. package/dist/hooks/runtime/usage-block.js.map +1 -1
  107. package/dist/hooks/runtime/user-prompt-submit.js +2 -1
  108. package/dist/hooks/runtime/user-prompt-submit.js.map +1 -1
  109. package/dist/hooks/runtime/wire-text.js +41 -0
  110. package/dist/hooks/runtime/wire-text.js.map +1 -0
  111. package/dist/hooks/runtime/work-dir.js +84 -0
  112. package/dist/hooks/runtime/work-dir.js.map +1 -0
  113. package/dist/index.js +45 -12
  114. package/dist/index.js.map +1 -1
  115. package/dist/install.js +174 -11
  116. package/dist/install.js.map +1 -1
  117. package/dist/link.js +74 -48
  118. package/dist/link.js.map +1 -1
  119. package/dist/login.js +77 -11
  120. package/dist/login.js.map +1 -1
  121. package/dist/mcp/auth/agent-sign-ins.js +174 -0
  122. package/dist/mcp/auth/agent-sign-ins.js.map +1 -0
  123. package/dist/mcp/auth/callback.js +200 -0
  124. package/dist/mcp/auth/callback.js.map +1 -0
  125. package/dist/mcp/auth/connect.js +392 -0
  126. package/dist/mcp/auth/connect.js.map +1 -0
  127. package/dist/mcp/auth/discovery.js +399 -0
  128. package/dist/mcp/auth/discovery.js.map +1 -0
  129. package/dist/mcp/auth/oauth.js +326 -0
  130. package/dist/mcp/auth/oauth.js.map +1 -0
  131. package/dist/mcp/auth/probe.js +128 -0
  132. package/dist/mcp/auth/probe.js.map +1 -0
  133. package/dist/mcp/auth/secrets.js +446 -0
  134. package/dist/mcp/auth/secrets.js.map +1 -0
  135. package/dist/mcp/auth/store.js +260 -0
  136. package/dist/mcp/auth/store.js.map +1 -0
  137. package/dist/mcp/auth/tokenSource.js +55 -0
  138. package/dist/mcp/auth/tokenSource.js.map +1 -0
  139. package/dist/mcp/config/atomicWrite.js +98 -0
  140. package/dist/mcp/config/atomicWrite.js.map +1 -0
  141. package/dist/mcp/config/clients.js +171 -0
  142. package/dist/mcp/config/clients.js.map +1 -0
  143. package/dist/mcp/config/cybedefend-servers.js +48 -0
  144. package/dist/mcp/config/cybedefend-servers.js.map +1 -0
  145. package/dist/mcp/config/discover.js +114 -0
  146. package/dist/mcp/config/discover.js.map +1 -0
  147. package/dist/mcp/config/hook.js +123 -0
  148. package/dist/mcp/config/hook.js.map +1 -0
  149. package/dist/mcp/config/index.js +11 -0
  150. package/dist/mcp/config/index.js.map +1 -0
  151. package/dist/mcp/config/install.js +132 -0
  152. package/dist/mcp/config/install.js.map +1 -0
  153. package/dist/mcp/config/installed.js +37 -0
  154. package/dist/mcp/config/installed.js.map +1 -0
  155. package/dist/mcp/config/jsonEdit.js +119 -0
  156. package/dist/mcp/config/jsonEdit.js.map +1 -0
  157. package/dist/mcp/config/lock.js +102 -0
  158. package/dist/mcp/config/lock.js.map +1 -0
  159. package/dist/mcp/config/proxyBinary.js +211 -0
  160. package/dist/mcp/config/proxyBinary.js.map +1 -0
  161. package/dist/mcp/config/rewrite.js +388 -0
  162. package/dist/mcp/config/rewrite.js.map +1 -0
  163. package/dist/mcp/config/types.js +34 -0
  164. package/dist/mcp/config/types.js.map +1 -0
  165. package/dist/mcp/proxy/audit.js +83 -0
  166. package/dist/mcp/proxy/audit.js.map +1 -0
  167. package/dist/mcp/proxy/authInterceptor.js +119 -0
  168. package/dist/mcp/proxy/authInterceptor.js.map +1 -0
  169. package/dist/mcp/proxy/expand.js +93 -0
  170. package/dist/mcp/proxy/expand.js.map +1 -0
  171. package/dist/mcp/proxy/inject.js +412 -0
  172. package/dist/mcp/proxy/inject.js.map +1 -0
  173. package/dist/mcp/proxy/intercept.js +63 -0
  174. package/dist/mcp/proxy/intercept.js.map +1 -0
  175. package/dist/mcp/proxy/jsonrpc.js +153 -0
  176. package/dist/mcp/proxy/jsonrpc.js.map +1 -0
  177. package/dist/mcp/proxy/log.js +109 -0
  178. package/dist/mcp/proxy/log.js.map +1 -0
  179. package/dist/mcp/proxy/paths.js +57 -0
  180. package/dist/mcp/proxy/paths.js.map +1 -0
  181. package/dist/mcp/proxy/proxy.js +520 -0
  182. package/dist/mcp/proxy/proxy.js.map +1 -0
  183. package/dist/mcp/proxy/serverCheckTimer.js +75 -0
  184. package/dist/mcp/proxy/serverCheckTimer.js.map +1 -0
  185. package/dist/mcp/proxy/sessionLifecycle.js +29 -0
  186. package/dist/mcp/proxy/sessionLifecycle.js.map +1 -0
  187. package/dist/mcp/proxy/spec.js +203 -0
  188. package/dist/mcp/proxy/spec.js.map +1 -0
  189. package/dist/mcp/proxy/sse.js +77 -0
  190. package/dist/mcp/proxy/sse.js.map +1 -0
  191. package/dist/mcp/proxy/upstream.js +2 -0
  192. package/dist/mcp/proxy/upstream.js.map +1 -0
  193. package/dist/mcp/proxy/upstreamHttp.js +499 -0
  194. package/dist/mcp/proxy/upstreamHttp.js.map +1 -0
  195. package/dist/mcp/proxy/upstreamStdio.js +188 -0
  196. package/dist/mcp/proxy/upstreamStdio.js.map +1 -0
  197. package/dist/mcp/proxy/upstreams.js +143 -0
  198. package/dist/mcp/proxy/upstreams.js.map +1 -0
  199. package/dist/mcp/proxy/verifyInterceptor.js +718 -0
  200. package/dist/mcp/proxy/verifyInterceptor.js.map +1 -0
  201. package/dist/mcp/proxy/windowsCommand.js +99 -0
  202. package/dist/mcp/proxy/windowsCommand.js.map +1 -0
  203. package/dist/mcp/report/buffer.js +351 -0
  204. package/dist/mcp/report/buffer.js.map +1 -0
  205. package/dist/mcp/report/coalesce.js +165 -0
  206. package/dist/mcp/report/coalesce.js.map +1 -0
  207. package/dist/mcp/report/constants.js +51 -0
  208. package/dist/mcp/report/constants.js.map +1 -0
  209. package/dist/mcp/report/project.js +83 -0
  210. package/dist/mcp/report/project.js.map +1 -0
  211. package/dist/mcp/report/reporter.js +443 -0
  212. package/dist/mcp/report/reporter.js.map +1 -0
  213. package/dist/mcp/report/types.js +3 -0
  214. package/dist/mcp/report/types.js.map +1 -0
  215. package/dist/mcp/report/validate.js +179 -0
  216. package/dist/mcp/report/validate.js.map +1 -0
  217. package/dist/mcp/scan/args.js +228 -0
  218. package/dist/mcp/scan/args.js.map +1 -0
  219. package/dist/mcp/scan/results.js +253 -0
  220. package/dist/mcp/scan/results.js.map +1 -0
  221. package/dist/mcp/scan/types.js +40 -0
  222. package/dist/mcp/scan/types.js.map +1 -0
  223. package/dist/mcp/verify/api.js +514 -0
  224. package/dist/mcp/verify/api.js.map +1 -0
  225. package/dist/mcp/verify/backend.js +143 -0
  226. package/dist/mcp/verify/backend.js.map +1 -0
  227. package/dist/mcp/verify/cache.js +200 -0
  228. package/dist/mcp/verify/cache.js.map +1 -0
  229. package/dist/mcp/verify/contract-errors.js +198 -0
  230. package/dist/mcp/verify/contract-errors.js.map +1 -0
  231. package/dist/mcp/verify/digest.js +91 -0
  232. package/dist/mcp/verify/digest.js.map +1 -0
  233. package/dist/mcp/verify/factory.js +56 -0
  234. package/dist/mcp/verify/factory.js.map +1 -0
  235. package/dist/mcp/verify/identity.js +286 -0
  236. package/dist/mcp/verify/identity.js.map +1 -0
  237. package/dist/mcp/verify/policy-store.js +63 -0
  238. package/dist/mcp/verify/policy-store.js.map +1 -0
  239. package/dist/mcp/verify/policy.js +160 -0
  240. package/dist/mcp/verify/policy.js.map +1 -0
  241. package/dist/mcp/verify/pool.js +27 -0
  242. package/dist/mcp/verify/pool.js.map +1 -0
  243. package/dist/mcp/verify/token-refresh.js +44 -0
  244. package/dist/mcp/verify/token-refresh.js.map +1 -0
  245. package/dist/mcp/verify/token.js +67 -0
  246. package/dist/mcp/verify/token.js.map +1 -0
  247. package/dist/mcp/verify/verifier.js +1166 -0
  248. package/dist/mcp/verify/verifier.js.map +1 -0
  249. package/dist/org-gate.js +1 -1
  250. package/dist/org-gate.js.map +1 -1
  251. package/dist/org.js +146 -0
  252. package/dist/org.js.map +1 -0
  253. package/dist/project-create.js +129 -0
  254. package/dist/project-create.js.map +1 -0
  255. package/dist/prompts.js +21 -0
  256. package/dist/prompts.js.map +1 -1
  257. package/dist/re2.wasm +0 -0
  258. package/dist/status.js +61 -0
  259. package/dist/status.js.map +1 -1
  260. package/dist/uninstall.js +49 -4
  261. package/dist/uninstall.js.map +1 -1
  262. package/dist/utils.js +62 -6
  263. package/dist/utils.js.map +1 -1
  264. package/package.json +10 -3
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Which CybeDefend backend this machine talks to, with what — and AS WHICH
3
+ * ORGANIZATION.
4
+ *
5
+ * The region lives in `~/.cybedefend/runtime-config.json`, written by
6
+ * `vibedefend install`. A machine that has never been installed has no
7
+ * region, no token, and therefore no verification — which is a supported
8
+ * state, not a failure: the proxy still forwards everything, and the log says
9
+ * why nothing was verified.
10
+ *
11
+ * The organization lives in the same file (`organization`), selected at
12
+ * `vibedefend login`, with `vibedefend org`, or here, lazily: the verification
13
+ * routes are org-scoped, most accounts belong to exactly one organization,
14
+ * and asking those users to pick would be a prompt with one answer. An
15
+ * account that belongs to several must choose explicitly, and until it does
16
+ * there is no backend to talk to — unverified, never an allow.
17
+ *
18
+ * `VIBEDEFEND_MCP_API_BASE` overrides the region's `apiBase`. It is how the
19
+ * tests point the proxy at a local mock, and it goes through the same origin
20
+ * allowlist as everything else in `api.ts` — a URL that is not trusted is
21
+ * refused before any request leaves, the organization listing included.
22
+ */
23
+ import { listOrganizations } from '../../api/projects.js';
24
+ import { REGIONS } from '../../config.js';
25
+ import { loadRuntimeConfig, saveRuntimeOrganization } from '../../hooks/runtime/config.js';
26
+ import { isTrustedApiBase } from '../../hooks/runtime/trusted-api-base.js';
27
+ import { PROXY_ENV_PREFIX } from '../proxy/paths.js';
28
+ import { resolveVerifyToken } from './token.js';
29
+ import { resolveVerifyTokenWithRefresh } from './token-refresh.js';
30
+ /** The region the installer recorded, or the EU default. */
31
+ export function activeRegion(deps = {}) {
32
+ try {
33
+ const runtime = (deps.loadRuntimeConfigFn ?? loadRuntimeConfig)();
34
+ const id = runtime?.region?.id;
35
+ const found = id ? REGIONS.find((region) => region.id === id) : undefined;
36
+ if (found)
37
+ return found;
38
+ // A custom region carries its own apiBase; it is honoured as it stands.
39
+ const custom = runtime?.region;
40
+ if (custom?.apiBase && custom.mcpName)
41
+ return custom;
42
+ }
43
+ catch {
44
+ /* no runtime config: fall through to the default */
45
+ }
46
+ return REGIONS[0];
47
+ }
48
+ /** The organization on file, without any network call. */
49
+ export function persistedOrganization(deps = {}) {
50
+ try {
51
+ const runtime = (deps.loadRuntimeConfigFn ?? loadRuntimeConfig)();
52
+ const organization = runtime?.organization;
53
+ if (organization && typeof organization.id === 'string' && organization.id !== '') {
54
+ return { id: organization.id, name: typeof organization.name === 'string' ? organization.name : '' };
55
+ }
56
+ }
57
+ catch {
58
+ /* no runtime config, or an unreadable one */
59
+ }
60
+ return null;
61
+ }
62
+ /**
63
+ * Everything the verifier needs to reach the backend, or `null` when there is
64
+ * nothing to reach it with — no token, or no organization to speak for.
65
+ */
66
+ export async function resolveBackendConfig(deps = {}) {
67
+ const env = deps.env ?? process.env;
68
+ const region = activeRegion(deps);
69
+ const apiBase = env[`${PROXY_ENV_PREFIX}API_BASE`]?.trim() || region.apiBase;
70
+ // Renewed if it expired: a human is waiting for this answer, and a stale
71
+ // token here turned a whole scan `unverified` (2026-09-16).
72
+ const token = await (deps.resolveTokenFn ?? resolveVerifyTokenWithRefresh)(region.mcpName, { env });
73
+ if (!token)
74
+ return null;
75
+ const organizationId = await resolveOrganizationId(apiBase, token, deps);
76
+ if (!organizationId)
77
+ return null;
78
+ return { apiBase, mcpName: region.mcpName, token, organizationId };
79
+ }
80
+ /**
81
+ * What the PROXY builds its verifier with — synchronously, and without a
82
+ * single network call.
83
+ *
84
+ * Section 0: nothing reaches CybeDefend before `initialize` is answered, and
85
+ * the verifier is wired before the proxy starts. So when no organization is
86
+ * persisted, the id is left to `resolveOrganizationId`, a thunk the verifier
87
+ * runs at its FIRST backend call — after `initialize`, on the `tools/list`
88
+ * path, where a network call already belongs. `scan`, `connect` and `org`
89
+ * are typed by a human and use `resolveBackendConfig` instead, which resolves
90
+ * up front.
91
+ */
92
+ export function resolveBackendAccess(deps = {}) {
93
+ const env = deps.env ?? process.env;
94
+ const region = activeRegion(deps);
95
+ const apiBase = env[`${PROXY_ENV_PREFIX}API_BASE`]?.trim() || region.apiBase;
96
+ const token = resolveVerifyToken(region.mcpName, { env });
97
+ if (!token)
98
+ return null;
99
+ const persisted = persistedOrganization(deps);
100
+ return {
101
+ apiBase,
102
+ mcpName: region.mcpName,
103
+ token,
104
+ ...(persisted
105
+ ? { organizationId: persisted.id }
106
+ : { resolveOrganizationId: () => resolveOrganizationId(apiBase, token, deps) }),
107
+ };
108
+ }
109
+ /**
110
+ * The persisted organization, or the account's only one — persisted on the
111
+ * way. Several, none, or an answer that could not be obtained: `null`, with
112
+ * one line naming the count, and nothing else. A guess here would be a
113
+ * verification under the wrong organization's policy.
114
+ */
115
+ export async function resolveOrganizationId(apiBase, token, deps) {
116
+ const persisted = persistedOrganization(deps);
117
+ if (persisted)
118
+ return persisted.id;
119
+ const warn = deps.warn ?? (() => undefined);
120
+ if (!isTrustedApiBase(apiBase)) {
121
+ warn('verify: apiBase is not a trusted origin — the organization was not resolved, nothing was sent');
122
+ return null;
123
+ }
124
+ const organizations = await (deps.listOrganizationsFn ?? listOrganizations)(apiBase, token);
125
+ if (organizations === null) {
126
+ warn('verify: could not list the organizations, no backend for this session');
127
+ return null;
128
+ }
129
+ if (organizations.length !== 1) {
130
+ warn('verify: no organization selected and the account belongs to several (or none); ' +
131
+ 'run `vibedefend org <id>` — no backend for this session', { organizations: organizations.length });
132
+ return null;
133
+ }
134
+ const only = organizations[0];
135
+ try {
136
+ (deps.saveOrganizationFn ?? saveRuntimeOrganization)(only);
137
+ }
138
+ catch {
139
+ /* remembered for this session at least; the next one resolves it again */
140
+ }
141
+ return only.id;
142
+ }
143
+ //# sourceMappingURL=backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend.js","sourceRoot":"","sources":["../../../src/mcp/verify/backend.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,200 @@
1
+ /**
2
+ * What we already know about a server and its tools, on this machine.
3
+ *
4
+ * One JSON file per server under `~/.cybedefend/mcp/cache/`. One file, not
5
+ * one database, for a reason the plan states: the agent spawns one proxy per
6
+ * MCP server and they all start at once, so a shared file would be five
7
+ * processes writing the same document within the same second. Per-server
8
+ * files never collide, and the interface below is narrow enough that moving
9
+ * to SQLite later is a swap rather than a rewrite.
10
+ *
11
+ * ─── The two rules that matter ──────────────────────────────────────────────
12
+ *
13
+ * - `ok` expires after seven days. A tool that was clean last week is
14
+ * probably still clean, and re-asking a backend about it on every session
15
+ * would make the common case the expensive one. Seven days bounds how
16
+ * stale that assumption can get.
17
+ * - `malicious` NEVER expires. Nothing about the passage of time makes a
18
+ * known-bad tool good again, and an expiry would silently reopen a server
19
+ * the user was protected from. Only an explicit `vibedefend allow` clears
20
+ * it.
21
+ *
22
+ * Overrides live in the same file as the verdicts they override, so a server
23
+ * the user allowed cannot end up half-allowed because two files disagreed.
24
+ */
25
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs';
26
+ import { join } from 'node:path';
27
+ import { writeFileAtomic } from '../config/atomicWrite.js';
28
+ import { mcpCacheDir } from '../proxy/paths.js';
29
+ import { serverIdFileName } from './identity.js';
30
+ import { isServerAction } from './policy.js';
31
+ /** Seven days on `ok`; `malicious` never expires. */
32
+ export const OK_TTL_MS = 7 * 24 * 60 * 60 * 1000;
33
+ export function sameStamp(a, b) {
34
+ if (a === null || b === null)
35
+ return a === b;
36
+ return a.mtimeMs === b.mtimeMs && a.size === b.size;
37
+ }
38
+ export function emptyCacheFile(serverId, now) {
39
+ const stamp = now.toISOString();
40
+ return {
41
+ version: 1,
42
+ serverId,
43
+ server: { firstSeen: stamp, lastSeen: stamp, verdict: 'unknown' },
44
+ tools: {},
45
+ };
46
+ }
47
+ export function createJsonVerifyCache(options = {}) {
48
+ const dir = options.dir ?? mcpCacheDir(options.env ?? process.env);
49
+ const pathFor = (serverId) => join(dir, serverIdFileName(serverId));
50
+ return {
51
+ read(serverId) {
52
+ let text;
53
+ try {
54
+ text = readFileSync(pathFor(serverId), 'utf8');
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ return parseCacheFile(text, serverId);
60
+ },
61
+ write(file) {
62
+ // 0600: a `reason` may quote a tool description, which is the content
63
+ // of a server the user has not decided to trust yet.
64
+ writeFileAtomic(pathFor(file.serverId), `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
65
+ },
66
+ stat(serverId) {
67
+ const stats = statSync(pathFor(serverId), { throwIfNoEntry: false });
68
+ return stats ? { mtimeMs: stats.mtimeMs, size: stats.size } : null;
69
+ },
70
+ list() {
71
+ if (!existsSync(dir))
72
+ return [];
73
+ const files = [];
74
+ for (const name of readdirSync(dir)) {
75
+ if (!name.endsWith('.json'))
76
+ continue;
77
+ try {
78
+ const parsed = parseCacheFile(readFileSync(join(dir, name), 'utf8'), '');
79
+ if (parsed)
80
+ files.push(parsed);
81
+ }
82
+ catch {
83
+ /* a corrupt file is one server we cannot report, not a failure */
84
+ }
85
+ }
86
+ return files;
87
+ },
88
+ };
89
+ }
90
+ /**
91
+ * Reads a cache file, or `null`.
92
+ *
93
+ * A cache is a performance aid, never a source of truth for something we
94
+ * could not otherwise obtain, so a corrupt one reads as "nothing known" and
95
+ * the next verification rewrites it. The one thing NOT dropped on the way is
96
+ * a `malicious` verdict: see `mergeVerdict`.
97
+ */
98
+ export function parseCacheFile(text, serverId) {
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(text);
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ if (typeof parsed !== 'object' || parsed === null)
107
+ return null;
108
+ const typed = parsed;
109
+ if (typed.version !== 1 || typeof typed.server !== 'object' || !typed.server)
110
+ return null;
111
+ const tools = {};
112
+ for (const [digest, record] of Object.entries(typed.tools ?? {})) {
113
+ if (typeof record !== 'object' || record === null)
114
+ continue;
115
+ const entry = record;
116
+ if (!isVerdict(entry.verdict))
117
+ continue;
118
+ // An action this binary cannot read is no action: the verdict decides.
119
+ if (entry.action !== undefined && !isServerAction(entry.action))
120
+ delete entry.action;
121
+ tools[digest] = entry;
122
+ }
123
+ return {
124
+ version: 1,
125
+ serverId: typed.serverId || serverId,
126
+ server: {
127
+ firstSeen: typed.server.firstSeen ?? new Date(0).toISOString(),
128
+ lastSeen: typed.server.lastSeen ?? new Date(0).toISOString(),
129
+ verdict: isVerdict(typed.server.verdict) ? typed.server.verdict : 'unknown',
130
+ reason: typed.server.reason,
131
+ verdictAt: typed.server.verdictAt,
132
+ action: isServerAction(typed.server.action) ? typed.server.action : undefined,
133
+ override: typed.server.override === 'allow' || typed.server.override === 'deny' ? typed.server.override : undefined,
134
+ overrideAt: typed.server.overrideAt,
135
+ organizationBlock: parseOrganizationBlock(typed.server.organizationBlock),
136
+ advisoryIds: parseAdvisoryIds(typed.server.advisoryIds),
137
+ toolDigests: Array.isArray(typed.server.toolDigests)
138
+ ? typed.server.toolDigests.filter((digest) => typeof digest === 'string')
139
+ : undefined,
140
+ },
141
+ tools,
142
+ };
143
+ }
144
+ function parseOrganizationBlock(value) {
145
+ if (typeof value !== 'object' || value === null)
146
+ return undefined;
147
+ const at = value.confirmedAt;
148
+ return typeof at === 'string' && !Number.isNaN(Date.parse(at)) ? { confirmedAt: at } : undefined;
149
+ }
150
+ /** Closed shape of an advisory id; also the bound of a report (`advisoryIds`). */
151
+ export const ADVISORY_ID_PATTERN = /^[A-Za-z0-9._:-]{1,64}$/;
152
+ export const MAX_ADVISORY_IDS = 10;
153
+ function parseAdvisoryIds(value) {
154
+ if (!Array.isArray(value))
155
+ return undefined;
156
+ return value
157
+ .filter((id) => typeof id === 'string' && ADVISORY_ID_PATTERN.test(id))
158
+ .slice(0, MAX_ADVISORY_IDS);
159
+ }
160
+ function isVerdict(value) {
161
+ return value === 'ok' || value === 'suspicious' || value === 'malicious' || value === 'pending' || value === 'unknown';
162
+ }
163
+ /** Has this record aged out? `malicious` never does. */
164
+ export function isExpired(record, now, ttlMs = OK_TTL_MS) {
165
+ if (record.verdict === 'malicious')
166
+ return false;
167
+ if (record.verdict !== 'ok')
168
+ return true;
169
+ if (!record.verdictAt)
170
+ return true;
171
+ const at = Date.parse(record.verdictAt);
172
+ return Number.isNaN(at) || now.getTime() - at > ttlMs;
173
+ }
174
+ /**
175
+ * Combines what we knew with what we just learned.
176
+ *
177
+ * The rule the previous generation arrived at and this keeps: a FRESH answer
178
+ * that says "not known" never overwrites a fact we already have, and a fresh
179
+ * answer that DOES know always wins. Without the first half, one backend
180
+ * outage would quietly clear every `malicious` on the machine; without the
181
+ * second, a tool that has just been reclassified would stay wrong for a week.
182
+ */
183
+ export function mergeVerdict(previous, next) {
184
+ if (!previous)
185
+ return next;
186
+ // The submission mark is a fact about what WE did, not a verdict: it
187
+ // survives whatever the fresh answer says, or the same digest would be
188
+ // bought again after every lookup that still came back "unknown".
189
+ const submittedAt = next.submittedAt ?? previous.submittedAt;
190
+ const marked = (record) => (submittedAt ? { ...record, submittedAt } : record);
191
+ if (next.verdict === 'unknown' || next.verdict === 'pending') {
192
+ return previous.verdict === 'unknown' || previous.verdict === 'pending' ? marked(next) : marked(previous);
193
+ }
194
+ return marked(next);
195
+ }
196
+ export function ensureCacheDir(options = {}) {
197
+ const dir = options.dir ?? mcpCacheDir(options.env ?? process.env);
198
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
199
+ }
200
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../../src/mcp/verify/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * What the backend refused, and since when.
3
+ *
4
+ * ── TWO FAMILIES OF FAILURE, AND ONLY ONE OF THEM IS FAIL-OPEN ──────────────
5
+ *
6
+ * A network error, a timeout or a 5xx means the backend could not answer. The
7
+ * proxy forwards the tool list, records `pending`, and says nothing to the
8
+ * agent: an outage must not turn every MCP server off, and the user cannot do
9
+ * anything about it anyway.
10
+ *
11
+ * A 400, 409 or 422 means the backend answered, and refused what we sent. That
12
+ * is not an outage, it is a DISAGREEMENT ABOUT THE CONTRACT — our request is
13
+ * malformed, or our digest formula is not one this server can compute. Filing
14
+ * it as `pending` would be a lie in the one direction that matters: `pending`
15
+ * says "we will retry and it will work", and retrying cannot help. Worse, it
16
+ * is indistinguishable from an ordinary outage, so a client that has stopped
17
+ * being able to verify ANYTHING looks exactly like a client waiting on a slow
18
+ * backend — for as long as nobody looks.
19
+ *
20
+ * ── WHY ITS OWN FILE, AND NOT THE PER-SERVER CACHE ─────────────────────────
21
+ *
22
+ * A contract error is not a property of the MCP server we happened to be
23
+ * asking about when it happened: it is a property of the client ↔ backend
24
+ * pair. A `409 DIGEST_VERSION_UNSUPPORTED` hits EVERY server, and writing it
25
+ * into the cache of whichever one we were looking at would draw a false map —
26
+ * one server "at fault", the others "clean", when in fact nothing is being
27
+ * verified anywhere.
28
+ *
29
+ * ── WHY SESSIONS AND NOT OCCURRENCES ───────────────────────────────────────
30
+ *
31
+ * The alert threshold is "it survived a restart". Counting occurrences would
32
+ * fire on a single session that listed three servers; counting distinct
33
+ * sessions asks the only question worth asking — did restarting fix it. Only
34
+ * the LAST session id is kept, not the set: the count is the answer, the list
35
+ * would only grow.
36
+ */
37
+ import { mkdirSync, readFileSync } from 'node:fs';
38
+ import { join } from 'node:path';
39
+ import { writeFileAtomic } from '../config/atomicWrite.js';
40
+ import { sanitise } from '../proxy/inject.js';
41
+ import { mcpHomeDir } from '../proxy/paths.js';
42
+ /**
43
+ * The statuses that mean "your request is the problem".
44
+ *
45
+ * `400` malformed, `422` unprocessable, `409` a state disagreement — which is
46
+ * what an unsupported digest version is. `403` and `404` joined them with the
47
+ * org-scoped routes: the organization is in the PATH, and "not a member" or
48
+ * "no such organization" is an answer about what this machine selected, not
49
+ * about the backend's health — retrying with the same organization cannot
50
+ * help. Everything else, 429 and 5xx included, describes something that
51
+ * happened to the BACKEND and stays fail-open.
52
+ */
53
+ export const CONTRACT_ERROR_STATUSES = [400, 403, 404, 409, 422];
54
+ export function isContractErrorStatus(status) {
55
+ return CONTRACT_ERROR_STATUSES.includes(status);
56
+ }
57
+ /**
58
+ * ── `code` AND `supported` COME OFF THE WIRE ────────────────────────────────
59
+ *
60
+ * They are fields of an HTTP response. They are written to disk here, and
61
+ * rendered into the user's terminal by `vibedefend status`. This repository
62
+ * already has a doctrine for that — every string that came from a server goes
63
+ * through `sanitise` before it reaches a reader — and this path had missed it
64
+ * because the fields look like metadata rather than prose.
65
+ *
66
+ * What it costs if forgotten: an ESC[2J clears the screen, a carriage return
67
+ * rewrites the line above, a newline forges a second status line. `apiBase`
68
+ * is gated by the origin allowlist, true — but "the source is trusted" is
69
+ * exactly the reasoning that cost the two defects found in the Phase 4
70
+ * end-of-session review.
71
+ *
72
+ * The bounds are the other half: the SENDER, not us, would otherwise choose
73
+ * how much we write to the user's disk on every single occurrence.
74
+ */
75
+ export const MAX_CONTRACT_CODE_CHARS = 64;
76
+ export const MAX_SUPPORTED_VERSIONS = 8;
77
+ export const MAX_SUPPORTED_VERSION_CHARS = 16;
78
+ /** One code, safe to store and to print. */
79
+ function safeCode(code) {
80
+ if (code === undefined)
81
+ return undefined;
82
+ const clean = sanitise(code, MAX_CONTRACT_CODE_CHARS);
83
+ return clean.length > 0 ? clean : undefined;
84
+ }
85
+ /** The supported-version list, bounded in both directions and sanitised. */
86
+ function safeSupported(supported) {
87
+ if (supported === undefined)
88
+ return undefined;
89
+ const clean = supported
90
+ .slice(0, MAX_SUPPORTED_VERSIONS)
91
+ .map((value) => sanitise(value, MAX_SUPPORTED_VERSION_CHARS))
92
+ .filter((value) => value.length > 0);
93
+ return clean.length > 0 ? clean : undefined;
94
+ }
95
+ /** Distinct sessions before the agent is told. See the header. */
96
+ export const PERSISTENT_AFTER_SESSIONS = 2;
97
+ export function contractErrorsFile(env = process.env) {
98
+ return join(mcpHomeDir(env), 'contract-errors.json');
99
+ }
100
+ /**
101
+ * The key of a record. Route + status + code, so an unsupported digest version
102
+ * and a malformed body on the same route stay two separate lines — they have
103
+ * two separate fixes.
104
+ */
105
+ function keyOf(input) {
106
+ return `${input.route}|${input.status}|${input.code ?? ''}`;
107
+ }
108
+ /**
109
+ * Reads the file, or an empty one.
110
+ *
111
+ * A malformed file reads as empty rather than throwing. Several proxies write
112
+ * this concurrently (one process per MCP server), and this is a JOURNAL: a
113
+ * corrupt journal must never be the reason a verification fails.
114
+ */
115
+ function read(env) {
116
+ try {
117
+ const parsed = JSON.parse(readFileSync(contractErrorsFile(env), 'utf8'));
118
+ if (!parsed || typeof parsed !== 'object')
119
+ return { version: 1, errors: {} };
120
+ const file = parsed;
121
+ if (!file.errors || typeof file.errors !== 'object')
122
+ return { version: 1, errors: {} };
123
+ return { version: 1, errors: file.errors };
124
+ }
125
+ catch {
126
+ return { version: 1, errors: {} };
127
+ }
128
+ }
129
+ /** Every contract error on file, worst-persisting first. */
130
+ export function listContractErrors(env = process.env) {
131
+ return Object.values(read(env).errors)
132
+ .map(({ lastSessionId: _ignored, ...record }) => record)
133
+ .sort((left, right) => right.sessions - left.sessions || left.route.localeCompare(right.route));
134
+ }
135
+ /** The ones that survived a restart — the threshold for telling the agent. */
136
+ export function persistentContractErrors(env = process.env) {
137
+ return listContractErrors(env).filter((record) => record.sessions >= PERSISTENT_AFTER_SESSIONS);
138
+ }
139
+ /**
140
+ * Files one occurrence.
141
+ *
142
+ * Best-effort by design: a failure to write must not take down the
143
+ * verification that was already failing. The caller is in a `catch`.
144
+ */
145
+ export function recordContractError(input, deps = {}) {
146
+ const env = deps.env ?? process.env;
147
+ const at = (deps.now ?? new Date()).toISOString();
148
+ // Sanitised HERE, at the one place wire data enters the record, rather than
149
+ // at each of the two places it leaves it. A guard on the way out is a guard
150
+ // the third reader forgets.
151
+ const code = safeCode(input.code);
152
+ const supported = safeSupported(input.supported);
153
+ try {
154
+ const file = read(env);
155
+ const key = keyOf({ ...input, code });
156
+ const existing = file.errors[key];
157
+ file.errors[key] = existing
158
+ ? {
159
+ ...existing,
160
+ // The code and the supported list can only get MORE precise: a
161
+ // backend that starts naming its refusal should not be ignored
162
+ // because the first occurrence was anonymous.
163
+ code: code ?? existing.code,
164
+ supported: supported ?? existing.supported,
165
+ lastSeenAt: at,
166
+ // Only a NEW session id advances the count. Three refusals inside
167
+ // one run are one session, which is the whole point.
168
+ sessions: existing.lastSessionId === input.sessionId ? existing.sessions : existing.sessions + 1,
169
+ lastSessionId: input.sessionId,
170
+ }
171
+ : {
172
+ route: input.route,
173
+ status: input.status,
174
+ code,
175
+ supported,
176
+ firstSeenAt: at,
177
+ lastSeenAt: at,
178
+ sessions: 1,
179
+ lastSessionId: input.sessionId,
180
+ };
181
+ mkdirSync(mcpHomeDir(env), { recursive: true });
182
+ writeFileAtomic(contractErrorsFile(env), `${JSON.stringify(file, null, 2)}\n`);
183
+ }
184
+ catch {
185
+ /* a journal that cannot be written is still not a reason to fail a check */
186
+ }
187
+ }
188
+ /** Clears the file. `vibedefend status` uses it once the disagreement is fixed. */
189
+ export function clearContractErrors(env = process.env) {
190
+ try {
191
+ mkdirSync(mcpHomeDir(env), { recursive: true });
192
+ writeFileAtomic(contractErrorsFile(env), `${JSON.stringify({ version: 1, errors: {} }, null, 2)}\n`);
193
+ }
194
+ catch {
195
+ /* nothing to do: the next successful call will not re-file anything */
196
+ }
197
+ }
198
+ //# sourceMappingURL=contract-errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-errors.js","sourceRoot":"","sources":["../../../src/mcp/verify/contract-errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Maximum nesting depth accepted by the canonicalisation.
4
+ *
5
+ * The depth is attacker-controlled and the canonicalisation recurses. On this
6
+ * engine the stack overflows around 2,500 levels, while a 5,000-level schema
7
+ * weighs only ~30 KB, far below any size cap. Without a bound the resulting
8
+ * `RangeError` was neither a business error nor a shape error: it became an
9
+ * internal failure that took the whole batch down with it.
10
+ *
11
+ * 100 is two orders of magnitude above any real JSON Schema (the deepest
12
+ * schemas MCP servers produce stay around twenty levels) and twenty-five times
13
+ * below the measured overflow: the bound refuses an attack, never a
14
+ * legitimate schema.
15
+ */
16
+ export const MAX_CANONICAL_DEPTH = 100;
17
+ /**
18
+ * A caller-side shape error, not a server failure. Kept as a dedicated class
19
+ * rather than a bare `RangeError` so a controller can map it to an
20
+ * invalid-argument response, exactly as it already does for a `SyntaxError`
21
+ * on a syntactically invalid schema. A genuine stack overflow stays a failure.
22
+ */
23
+ export class ToolSchemaTooDeepError extends Error {
24
+ maxDepth;
25
+ constructor(maxDepth) {
26
+ super(`tool schema nesting exceeds ${maxDepth} levels`);
27
+ this.maxDepth = maxDepth;
28
+ this.name = 'ToolSchemaTooDeepError';
29
+ }
30
+ }
31
+ /**
32
+ * JSON with keys sorted at every depth: two payloads that differ only by key
33
+ * order produce a single string. Arrays keep their order, because in JSON
34
+ * Schema `required` and `enum` are ordered data.
35
+ *
36
+ * Depth-bounded: see `MAX_CANONICAL_DEPTH`.
37
+ */
38
+ export function stableStringify(value) {
39
+ return canonicalize(value, 0);
40
+ }
41
+ /**
42
+ * The depth is passed explicitly, never through a default parameter exposed to
43
+ * `Array.prototype.map`: `value.map(canonicalize)` would pass the array INDEX
44
+ * as the second argument and reset the bound on every first element, a guard
45
+ * that compiles, passes nominal tests, and guards nothing.
46
+ */
47
+ function canonicalize(value, depth) {
48
+ if (depth > MAX_CANONICAL_DEPTH) {
49
+ throw new ToolSchemaTooDeepError(MAX_CANONICAL_DEPTH);
50
+ }
51
+ if (value === null || typeof value !== 'object')
52
+ return JSON.stringify(value) ?? 'null';
53
+ if (Array.isArray(value))
54
+ return `[${value.map((item) => canonicalize(item, depth + 1)).join(',')}]`;
55
+ const entries = Object.keys(value)
56
+ .sort()
57
+ .map((k) => `${JSON.stringify(k)}:${canonicalize(value[k], depth + 1)}`);
58
+ return `{${entries.join(',')}}`;
59
+ }
60
+ /**
61
+ * Version of the digest formula, carried as a prefix of every digest
62
+ * (`d1:<sha256 hex>`). The digest is shared across organisations and
63
+ * repositories: bump this when the formula changes so old verdicts are
64
+ * ignored rather than silently mismatched.
65
+ */
66
+ export const DIGEST_VERSION = 'd1';
67
+ /**
68
+ * Digest of a tool definition: exactly the surface the agent consumes and the
69
+ * attacker controls. `title` and `annotations` are included on purpose: the
70
+ * agent reads them too (a lying `destructiveHint: false`, for instance).
71
+ *
72
+ * No whitespace normalisation. This digest is the cache key of the backend
73
+ * judge, and the judge reads the RAW text: two descriptions it would read
74
+ * differently must never share a verdict.
75
+ *
76
+ * Shared contract with the backend: `tests/mcp/verify/__fixtures__/digest-vectors.json`
77
+ * pins the expected output on both sides.
78
+ */
79
+ export function toolDigest(tool) {
80
+ const canonical = stableStringify({
81
+ name: tool.name ?? '',
82
+ title: tool.title ?? '',
83
+ description: tool.description ?? '',
84
+ inputSchema: tool.inputSchema ?? null,
85
+ outputSchema: tool.outputSchema ?? null,
86
+ annotations: tool.annotations ?? null,
87
+ });
88
+ const hex = createHash('sha256').update(canonical, 'utf8').digest('hex');
89
+ return `${DIGEST_VERSION}:${hex}`;
90
+ }
91
+ //# sourceMappingURL=digest.js.map