@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.
- package/README.md +71 -2
- package/dist/api/gateway-request.js +49 -0
- package/dist/api/gateway-request.js.map +1 -0
- package/dist/api/projects.js +67 -0
- package/dist/api/projects.js.map +1 -1
- package/dist/api/repository-link.js +157 -0
- package/dist/api/repository-link.js.map +1 -0
- package/dist/auth/auth-store.js +107 -25
- package/dist/auth/auth-store.js.map +1 -1
- package/dist/auth/auth.js +21 -0
- package/dist/auth/auth.js.map +1 -1
- package/dist/clients/cursor.js +5 -1
- package/dist/clients/cursor.js.map +1 -1
- package/dist/clients/vscode.js +38 -29
- package/dist/clients/vscode.js.map +1 -1
- package/dist/commands/mcp/allow.js +219 -0
- package/dist/commands/mcp/allow.js.map +1 -0
- package/dist/commands/mcp/connect.js +337 -0
- package/dist/commands/mcp/connect.js.map +1 -0
- package/dist/commands/mcp/index.js +170 -0
- package/dist/commands/mcp/index.js.map +1 -0
- package/dist/commands/mcp/proxy.js +320 -0
- package/dist/commands/mcp/proxy.js.map +1 -0
- package/dist/commands/mcp/scan.js +349 -0
- package/dist/commands/mcp/scan.js.map +1 -0
- package/dist/commands/mcp/sync.js +215 -0
- package/dist/commands/mcp/sync.js.map +1 -0
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/diagnostics.js +78 -12
- package/dist/diagnostics.js.map +1 -1
- package/dist/doctor.js +19 -0
- package/dist/doctor.js.map +1 -1
- package/dist/guards-evaluator/analysis-types.js +27 -0
- package/dist/guards-evaluator/analysis-types.js.map +1 -0
- package/dist/guards-evaluator/command-analysis.js +572 -0
- package/dist/guards-evaluator/command-analysis.js.map +1 -0
- package/dist/guards-evaluator/command-view.js +586 -0
- package/dist/guards-evaluator/command-view.js.map +1 -0
- package/dist/guards-evaluator/env.js +11 -0
- package/dist/guards-evaluator/env.js.map +1 -0
- package/dist/guards-evaluator/evaluate.js +91 -41
- package/dist/guards-evaluator/evaluate.js.map +1 -1
- package/dist/guards-evaluator/index.js +2 -1
- package/dist/guards-evaluator/index.js.map +1 -1
- package/dist/guards-evaluator/inline-code.js +259 -0
- package/dist/guards-evaluator/inline-code.js.map +1 -0
- package/dist/guards-evaluator/normalize.js +716 -0
- package/dist/guards-evaluator/normalize.js.map +1 -0
- package/dist/guards-evaluator/preprocess.js +13 -5
- package/dist/guards-evaluator/preprocess.js.map +1 -1
- package/dist/guards-evaluator/redact.js +19 -0
- package/dist/guards-evaluator/redact.js.map +1 -1
- package/dist/guards-evaluator/regex.js +25 -3
- package/dist/guards-evaluator/regex.js.map +1 -1
- package/dist/guards-evaluator/safe-commands.js +155 -0
- package/dist/guards-evaluator/safe-commands.js.map +1 -0
- package/dist/guards-evaluator/triage.js +315 -0
- package/dist/guards-evaluator/triage.js.map +1 -0
- package/dist/hook-runner.js +24679 -5079
- package/dist/hooks/install.js +38 -0
- package/dist/hooks/install.js.map +1 -1
- package/dist/hooks/runtime/config.js +38 -0
- package/dist/hooks/runtime/config.js.map +1 -1
- package/dist/hooks/runtime/emit.js +94 -61
- package/dist/hooks/runtime/emit.js.map +1 -1
- package/dist/hooks/runtime/fetch-rules.js +22 -7
- package/dist/hooks/runtime/fetch-rules.js.map +1 -1
- package/dist/hooks/runtime/gateway-http-error.js +34 -0
- package/dist/hooks/runtime/gateway-http-error.js.map +1 -0
- package/dist/hooks/runtime/guard-rules-cache.js +139 -17
- package/dist/hooks/runtime/guard-rules-cache.js.map +1 -1
- package/dist/hooks/runtime/guard-violations-buffer.js +321 -48
- package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -1
- package/dist/hooks/runtime/judge-cache.js +6 -0
- package/dist/hooks/runtime/judge-cache.js.map +1 -1
- package/dist/hooks/runtime/judge-triage.js +193 -0
- package/dist/hooks/runtime/judge-triage.js.map +1 -0
- package/dist/hooks/runtime/judge.js +7 -1
- package/dist/hooks/runtime/judge.js.map +1 -1
- package/dist/hooks/runtime/pre-compact.js +12 -6
- package/dist/hooks/runtime/pre-compact.js.map +1 -1
- package/dist/hooks/runtime/resolve.js +90 -8
- package/dist/hooks/runtime/resolve.js.map +1 -1
- package/dist/hooks/runtime/retry-policy.js +57 -0
- package/dist/hooks/runtime/retry-policy.js.map +1 -0
- package/dist/hooks/runtime/review-sessions.js +74 -0
- package/dist/hooks/runtime/review-sessions.js.map +1 -0
- package/dist/hooks/runtime/script-resolve.js +695 -0
- package/dist/hooks/runtime/script-resolve.js.map +1 -0
- package/dist/hooks/runtime/session-activation.js +5 -1
- package/dist/hooks/runtime/session-activation.js.map +1 -1
- package/dist/hooks/runtime/session-review.js +163 -45
- package/dist/hooks/runtime/session-review.js.map +1 -1
- package/dist/hooks/runtime/session-start.js +2 -1
- package/dist/hooks/runtime/session-start.js.map +1 -1
- package/dist/hooks/runtime/session-writes.js +232 -0
- package/dist/hooks/runtime/session-writes.js.map +1 -0
- package/dist/hooks/runtime/sniff.js +26 -7
- package/dist/hooks/runtime/sniff.js.map +1 -1
- package/dist/hooks/runtime/target-resolve.js +336 -0
- package/dist/hooks/runtime/target-resolve.js.map +1 -0
- package/dist/hooks/runtime/unmapped-tools.js +93 -0
- package/dist/hooks/runtime/unmapped-tools.js.map +1 -0
- package/dist/hooks/runtime/usage-block.js +4 -0
- package/dist/hooks/runtime/usage-block.js.map +1 -1
- package/dist/hooks/runtime/user-prompt-submit.js +2 -1
- package/dist/hooks/runtime/user-prompt-submit.js.map +1 -1
- package/dist/hooks/runtime/wire-text.js +41 -0
- package/dist/hooks/runtime/wire-text.js.map +1 -0
- package/dist/hooks/runtime/work-dir.js +84 -0
- package/dist/hooks/runtime/work-dir.js.map +1 -0
- package/dist/index.js +45 -12
- package/dist/index.js.map +1 -1
- package/dist/install.js +174 -11
- package/dist/install.js.map +1 -1
- package/dist/link.js +74 -48
- package/dist/link.js.map +1 -1
- package/dist/login.js +77 -11
- package/dist/login.js.map +1 -1
- package/dist/mcp/auth/agent-sign-ins.js +174 -0
- package/dist/mcp/auth/agent-sign-ins.js.map +1 -0
- package/dist/mcp/auth/callback.js +200 -0
- package/dist/mcp/auth/callback.js.map +1 -0
- package/dist/mcp/auth/connect.js +392 -0
- package/dist/mcp/auth/connect.js.map +1 -0
- package/dist/mcp/auth/discovery.js +399 -0
- package/dist/mcp/auth/discovery.js.map +1 -0
- package/dist/mcp/auth/oauth.js +326 -0
- package/dist/mcp/auth/oauth.js.map +1 -0
- package/dist/mcp/auth/probe.js +128 -0
- package/dist/mcp/auth/probe.js.map +1 -0
- package/dist/mcp/auth/secrets.js +446 -0
- package/dist/mcp/auth/secrets.js.map +1 -0
- package/dist/mcp/auth/store.js +260 -0
- package/dist/mcp/auth/store.js.map +1 -0
- package/dist/mcp/auth/tokenSource.js +55 -0
- package/dist/mcp/auth/tokenSource.js.map +1 -0
- package/dist/mcp/config/atomicWrite.js +98 -0
- package/dist/mcp/config/atomicWrite.js.map +1 -0
- package/dist/mcp/config/clients.js +171 -0
- package/dist/mcp/config/clients.js.map +1 -0
- package/dist/mcp/config/cybedefend-servers.js +48 -0
- package/dist/mcp/config/cybedefend-servers.js.map +1 -0
- package/dist/mcp/config/discover.js +114 -0
- package/dist/mcp/config/discover.js.map +1 -0
- package/dist/mcp/config/hook.js +123 -0
- package/dist/mcp/config/hook.js.map +1 -0
- package/dist/mcp/config/index.js +11 -0
- package/dist/mcp/config/index.js.map +1 -0
- package/dist/mcp/config/install.js +132 -0
- package/dist/mcp/config/install.js.map +1 -0
- package/dist/mcp/config/installed.js +37 -0
- package/dist/mcp/config/installed.js.map +1 -0
- package/dist/mcp/config/jsonEdit.js +119 -0
- package/dist/mcp/config/jsonEdit.js.map +1 -0
- package/dist/mcp/config/lock.js +102 -0
- package/dist/mcp/config/lock.js.map +1 -0
- package/dist/mcp/config/proxyBinary.js +211 -0
- package/dist/mcp/config/proxyBinary.js.map +1 -0
- package/dist/mcp/config/rewrite.js +388 -0
- package/dist/mcp/config/rewrite.js.map +1 -0
- package/dist/mcp/config/types.js +34 -0
- package/dist/mcp/config/types.js.map +1 -0
- package/dist/mcp/proxy/audit.js +83 -0
- package/dist/mcp/proxy/audit.js.map +1 -0
- package/dist/mcp/proxy/authInterceptor.js +119 -0
- package/dist/mcp/proxy/authInterceptor.js.map +1 -0
- package/dist/mcp/proxy/expand.js +93 -0
- package/dist/mcp/proxy/expand.js.map +1 -0
- package/dist/mcp/proxy/inject.js +412 -0
- package/dist/mcp/proxy/inject.js.map +1 -0
- package/dist/mcp/proxy/intercept.js +63 -0
- package/dist/mcp/proxy/intercept.js.map +1 -0
- package/dist/mcp/proxy/jsonrpc.js +153 -0
- package/dist/mcp/proxy/jsonrpc.js.map +1 -0
- package/dist/mcp/proxy/log.js +109 -0
- package/dist/mcp/proxy/log.js.map +1 -0
- package/dist/mcp/proxy/paths.js +57 -0
- package/dist/mcp/proxy/paths.js.map +1 -0
- package/dist/mcp/proxy/proxy.js +520 -0
- package/dist/mcp/proxy/proxy.js.map +1 -0
- package/dist/mcp/proxy/serverCheckTimer.js +75 -0
- package/dist/mcp/proxy/serverCheckTimer.js.map +1 -0
- package/dist/mcp/proxy/sessionLifecycle.js +29 -0
- package/dist/mcp/proxy/sessionLifecycle.js.map +1 -0
- package/dist/mcp/proxy/spec.js +203 -0
- package/dist/mcp/proxy/spec.js.map +1 -0
- package/dist/mcp/proxy/sse.js +77 -0
- package/dist/mcp/proxy/sse.js.map +1 -0
- package/dist/mcp/proxy/upstream.js +2 -0
- package/dist/mcp/proxy/upstream.js.map +1 -0
- package/dist/mcp/proxy/upstreamHttp.js +499 -0
- package/dist/mcp/proxy/upstreamHttp.js.map +1 -0
- package/dist/mcp/proxy/upstreamStdio.js +188 -0
- package/dist/mcp/proxy/upstreamStdio.js.map +1 -0
- package/dist/mcp/proxy/upstreams.js +143 -0
- package/dist/mcp/proxy/upstreams.js.map +1 -0
- package/dist/mcp/proxy/verifyInterceptor.js +718 -0
- package/dist/mcp/proxy/verifyInterceptor.js.map +1 -0
- package/dist/mcp/proxy/windowsCommand.js +99 -0
- package/dist/mcp/proxy/windowsCommand.js.map +1 -0
- package/dist/mcp/report/buffer.js +351 -0
- package/dist/mcp/report/buffer.js.map +1 -0
- package/dist/mcp/report/coalesce.js +165 -0
- package/dist/mcp/report/coalesce.js.map +1 -0
- package/dist/mcp/report/constants.js +51 -0
- package/dist/mcp/report/constants.js.map +1 -0
- package/dist/mcp/report/project.js +83 -0
- package/dist/mcp/report/project.js.map +1 -0
- package/dist/mcp/report/reporter.js +443 -0
- package/dist/mcp/report/reporter.js.map +1 -0
- package/dist/mcp/report/types.js +3 -0
- package/dist/mcp/report/types.js.map +1 -0
- package/dist/mcp/report/validate.js +179 -0
- package/dist/mcp/report/validate.js.map +1 -0
- package/dist/mcp/scan/args.js +228 -0
- package/dist/mcp/scan/args.js.map +1 -0
- package/dist/mcp/scan/results.js +253 -0
- package/dist/mcp/scan/results.js.map +1 -0
- package/dist/mcp/scan/types.js +40 -0
- package/dist/mcp/scan/types.js.map +1 -0
- package/dist/mcp/verify/api.js +514 -0
- package/dist/mcp/verify/api.js.map +1 -0
- package/dist/mcp/verify/backend.js +143 -0
- package/dist/mcp/verify/backend.js.map +1 -0
- package/dist/mcp/verify/cache.js +200 -0
- package/dist/mcp/verify/cache.js.map +1 -0
- package/dist/mcp/verify/contract-errors.js +198 -0
- package/dist/mcp/verify/contract-errors.js.map +1 -0
- package/dist/mcp/verify/digest.js +91 -0
- package/dist/mcp/verify/digest.js.map +1 -0
- package/dist/mcp/verify/factory.js +56 -0
- package/dist/mcp/verify/factory.js.map +1 -0
- package/dist/mcp/verify/identity.js +286 -0
- package/dist/mcp/verify/identity.js.map +1 -0
- package/dist/mcp/verify/policy-store.js +63 -0
- package/dist/mcp/verify/policy-store.js.map +1 -0
- package/dist/mcp/verify/policy.js +160 -0
- package/dist/mcp/verify/policy.js.map +1 -0
- package/dist/mcp/verify/pool.js +27 -0
- package/dist/mcp/verify/pool.js.map +1 -0
- package/dist/mcp/verify/token-refresh.js +44 -0
- package/dist/mcp/verify/token-refresh.js.map +1 -0
- package/dist/mcp/verify/token.js +67 -0
- package/dist/mcp/verify/token.js.map +1 -0
- package/dist/mcp/verify/verifier.js +1166 -0
- package/dist/mcp/verify/verifier.js.map +1 -0
- package/dist/org-gate.js +1 -1
- package/dist/org-gate.js.map +1 -1
- package/dist/org.js +146 -0
- package/dist/org.js.map +1 -0
- package/dist/project-create.js +129 -0
- package/dist/project-create.js.map +1 -0
- package/dist/prompts.js +21 -0
- package/dist/prompts.js.map +1 -1
- package/dist/re2.wasm +0 -0
- package/dist/status.js +61 -0
- package/dist/status.js.map +1 -1
- package/dist/uninstall.js +49 -4
- package/dist/uninstall.js.map +1 -1
- package/dist/utils.js +62 -6
- package/dist/utils.js.map +1 -1
- 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
|