1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
//! Per-tenant executor dispatch, shared by every transport that executes GraphQL.
//!
//! Resolving which tenant a request belongs to, refusing an unregistered key,
//! refusing a suspended tenant and charging the tenant's quotas is one policy. It
//! used to be written out inline in the `/graphql` handler and nowhere else, so
//! the MCP transport — mounted from the same [`AppState`] — captured the default
//! executor at session construction and never consulted the registry at all: an
//! authenticated MCP caller read the boot database rather than their own tenant's,
//! and a suspended tenant kept working over MCP while `/graphql` correctly
//! answered 503 (#858).
//!
//! Both steps live here so a control added to one transport is a control on both.
//! They are two functions rather than one because the caller distinguishes their
//! failures: a malformed `X-Tenant-ID` is the client's mistake (400) while an
//! unregistered or suspended tenant is a dispatch decision (403 / 503 / 429).
use Arc;
use HeaderMap;
use ;
use ;
/// The executor a request must run on, plus the quota permits it holds.
/// Resolve the tenant key for a request from its security context and headers.
///
/// Strict cross-source validation is enabled exactly when the compiled schema
/// configures RLS, so a multi-tenant deployment cannot be addressed with
/// conflicting tenant hints.
///
/// # Errors
///
/// Returns `FraiseQLError::Validation` when the `X-Tenant-ID` header is malformed,
/// or when strict validation is on and the available sources disagree.
/// Dispatch to the resolved tenant's executor and charge its quotas.
///
/// - `None` key → the default executor, unlimited.
/// - registered + active → the tenant's own executor, holding a concurrency permit and having
/// consumed one request from the per-second window.
/// - registered + suspended → `ServiceUnavailable`.
/// - not registered → `Authorization`. Never a silent fallback to the default executor, which would
/// serve another tenant's data.
///
/// # Errors
///
/// Propagates the registry's decision: `Authorization` for an unregistered key,
/// `ServiceUnavailable` for a suspended tenant, `RateLimited` when a quota is
/// exhausted.
/// Estimate the cost of `document` for budget enforcement and observability
/// (#379).
///
/// Returns `None` for a document that does not parse — rejection is left for
/// the executor's own parse-error path. Computed unconditionally per request
/// (one extra parse of an already-size-limited string) so an operator can
/// observe real traffic costs *before* configuring any budget: the number that
/// sizes `[security.cost_budget]` has to exist prior to enforcement.
/// Charge the tenant's cost budgets with a precomputed `cost` (#379).
///
/// Only an explicitly-keyed, registered tenant carries budgets. `cost` is
/// `None` for an unparseable document, which is left for the executor to
/// reject.
///
/// Lives beside the other two because it is the fourth per-tenant quota and
/// belongs to the same chokepoint; it is separate only because it needs the
/// document's cost.
///
/// **Deliberately not called by the MCP transport.** `estimate_query_cost` scores
/// the *shape* of the document — root fields against
/// `operation_cost_weights`, and the selection set — none of which an MCP caller
/// can vary: the document is built from the schema, carries exactly one root
/// field and a fixed scalar projection, and argument values travel as variables
/// (#808). The score for a given tool is therefore constant, so the check would
/// either always pass or permanently disable that tool for a budgeted tenant,
/// rather than metering anything. Volume over MCP is bounded by the concurrency
/// permit and the per-second limiter in [`dispatch_to_tenant`], which do apply.
/// (The schema-wide `[security.cost_budget] per_request_max` is different: it
/// is the operator capping their own schema, enforced inside the executor for
/// every transport including MCP.) If a future MCP surface lets the caller
/// shape the document, this is the call to add.
///
/// # Errors
///
/// Returns `FraiseQLError::CostExceeded` when `cost` exceeds the tenant's
/// per-request budget (no retry hint) or exhausts its per-minute window
/// (`Retry-After` hint).